0%

文件生成

通过 tsc --init 命令生成。

配置注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
/****** 根选项 ******/
"include": ["./src/**/*"], // 指定被编译文件所在的目录,** 表示任意目录,* 表示任意文件
"exclude": [], // 指定不需要被编译的目录
files: ["demo.ts"], // 指定被编译的文件


/****** 项目选项 ******/
"compilerOptions": {
"target": "ES6", // 目标语言的版本
"module": "commonjs", // 生成代码的模板标准
"lib": ["DOM", "ES5", "ES6", "ES7", "ScriptHost"], // TS需要引用的库
"outDir": "./dist", // 指定输出目录
"rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
"allowJs": true, // 允许编译器编译JS,JSX文件
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
"removeComments": true, // 删除注释
"esModuleInterop": true, // 允许export=导出,由import from 导入

/****** 严格检查选项 ******/
"strict": true, // 开启所有严格的类型检查
"alwaysStrict": true, // 在代码中注入'use strict'
"noImplicitAny": true, // 不允许隐式的any类型
"noImplicitThis": true, // 不允许this有隐式的any类型
"strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
"strictBindCallApply": true, // 严格的bind/call/apply检查
"strictFunctionTypes": true, // 不允许函数参数双向协变
"strictPropertyInitialization": true, // 类的实例属性必须初始化

/****** 额外检查 ******/
"noUnusedLocals": true, //是否检查未使用的局部变量
"noUnusedParameters": true, //是否检查未使用的参数
"noImplicitReturns": true, //检查函数是否不含有隐式返回值
"noImplicitOverride": true, //是否检查子类继承自基类时,其重载的函数命名与基类的函数不同步问题
"noFallthroughCasesInSwitch": true, //检查switch中是否含有case没有使用break跳出
"noUncheckedIndexedAccess": true, //是否通过索引签名来描述对象上有未知键但已知值的对象
"noPropertyAccessFromIndexSignature": true, //是否通过" . “(obj.key) 语法访问字段和"索引”( obj[“key”]), 以及在类型中声明属性的方式之间的一致性

/****** 实验选项 ******/
"experimentalDecorators": true, //是否启用对装饰器的实验性支持,装饰器是一种语言特性,还没有完全被 JavaScript 规范批准
"emitDecoratorMetadata": true, //为装饰器启用对发出类型元数据的实验性支持

/****** 高级选项 ******/
"forceConsistentCasingInFileNames": true, //是否区分文件系统大小写规则
"extendedDiagnostics": false, //是否查看 TS 在编译时花费的时间
"noEmitOnError": true, //有错误时不进行编译
"resolveJsonModule": true, //是否解析 JSON 模块
},
}

常用配置

  1. include
    指定编译文件,默认是编译当前目录下所有的ts文件

  2. exclude
    指定排除的文件,即不编译指定的文件

  3. target
    指定编译 js 的版本,常用的有 es5 和 es6

  4. allowJS
    是否允许编译js文件

  5. removeComments
    是否在编译过程中删除文件中的注释

  6. rootDir
    编译文件的目录

  7. outDir
    输出的目录

  8. sourceMap
    代码源文件

  9. strict
    严格模式

  10. module
    默认 common.js,可选 es6模式 amd 和 umd 等

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

软件工程的一个主要部分是构建组件,这些组件不仅具有定义明确且一致的 API,而且还可以重用。能够处理今天和明天的数据的组件将为你提供构建大型软件系统的最灵活的能力。

简单写法

1
2
3
4
5
6
7
8
9
10
11
// 不使用泛型
function getName01(name: string): string {
return name
}
console.log(getName01('翠花')) // 翠花

// 使用泛型
function getName02<T>(name: T): T {
return name
}
console.log(getName02<string>('翠花')) // 翠花

从上面的例子我们可以知道,如果使用了泛型,可以在调用该函数时再指定类型。比如我们在获取名字时,发现有些人并没有使用真实姓名,而是使用了 数字代号 作为名字,这时我们应该怎么解决呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不使用泛型
function getName01(name: string | number): string | number {
return name
}
console.log(getName01('翠花')) // 翠花
console.log(getName01(123)) // 123

// 使用泛型
function getName02<T>(name: T): T {
return name
}
console.log(getName02<string>('翠花')) // 翠花
console.log(getName02<number>(123)) // 123

结论:使用泛型更灵活。

泛型函数

1
2
3
4
function getName03<T>(name: T): T {
return name
}
console.log(getName03<string>('隔壁老王')) // 隔壁老王

注意:<T> 中的 T 可以是其他合法的字母,建议使用大写字母。

泛型接口

1
2
3
4
5
6
7
8
interface GetName04 {
<T>(name: T): T
}
function getName05<T>(name: T): T {
return name
}
let my_name: GetName04 = getName05
console.log(my_name('小红')) // 小红

复杂点的用法

1
2
3
4
5
6
7
8
9
interface GetName06 {
<T>(name: T): T;
<U>(age: U): U;
}
function getName07<T, U>(args: { name: T, age: U }) : { name: T, age: U } {
return args
}
let my_name_age: GetName06 = getName07
console.log(my_name_age({ name: '小红', age: 18 })) // { name: '小红', age: 18 }

泛型类

1
2
3
4
5
6
7
8
9
10
11
class Add<T> {
value: T;
addFn: (a: T, b: T) => T
}
let add_a_b = new Add<number>()
add_a_b.value = 666
console.log(add_a_b.value) // 666
add_a_b.addFn = (a, b) => {
return a + b
}
console.log(add_a_b.addFn(2, 3)) // 5

泛型约束

先看一个例子

1
2
3
4
function getName08<T>(name: T): T {
return `我是${name}` // 不能将类型“string”分配给类型“T”。
}
console.log(getName08<string>('隔壁老王'))

我们在使用泛型函数时,想要返回 我是${name} 时会报错,这是因为该函数返回的结果是字符串,而 nameT 类型,也就是未知类型,这时就会产生冲突了,有的朋友为了减少麻烦,可能会简单粗暴使用 any ,如下:

1
2
3
4
function getName08<T>(name: T): any {
return `我是${name}`
}
console.log(getName08<string>('隔壁老王')) // 我是隔壁老王

这确实可以解决问题,但是却失去了使用泛型的意义,这时最佳的解决方法是使用 泛型约束,如下:

1
2
3
4
5
6
7
interface Str {
name: String
}
function getName08<T extends Str>(person: T): any {
return `我是${person.name}呀`
}
console.log(getName08({ name: '隔壁老王'})) // 我是隔壁老王呀

使用 keyof 约束对象

我们可以声明受另一个类型参数约束的类型类型参数。如下,U (search) 继承并受到了 T (obj) 的约束,这确保我们不会获取到除 T (obj) 上不存在的属性,而是从 T (obj) 中获取其中的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getObj<T, U extends keyof T>(obj: T, search: U) {
return obj[search]
}
const students = {
1: { name: '张三', age: 16 },
2: { name: '李四', age: 17 },
3: { name: '王五', age: 18 },
4: { name: '赵六', age: 19 },
5: { name: '老七', age: 20 }
}
console.log(getObj(students, 3)) // { name: '王五', age: 18 }
console.log(getObj(students, 5)) // { name: '老七', age: 20 }
console.log(getObj(students, 6)) // 类型“6”的参数不能赋给类型“2 | 3 | 1 | 4 | 5”的参数。

链接:Typescript中文官网,Symbols篇

symbolES6 新增的一种基本数据类型,它和 numberstringbooleanundefinednull 是同类型的,object 是引用类型。它用来表示独一无二的值,通过 Symbol 函数生成。

创建 symbol类型

symbol类型的值是通过Symbol构造函数创建的。

1
2
const a = Symbol()
console.log(typeof a) // symbol

Symbol的值是唯一

1
2
3
let b = Symbol('key')
let c = Symbol('key')
console.log(b === c) // false

用做对象属性的键

1
2
3
4
5
let d = Symbol()
let e = {
[d]: 'value'
}
console.log(e[d]) // value

声明对象的属性和类成员

1
2
3
4
5
6
7
8
const f = Symbol()
class G {
[f]() {
return 'value'
}
}
const h = new G()
console.log(h[f]()) // value

symbol值可以转为字符串和布尔类型值

1
2
3
const i = Symbol('i')
console.log(i.toString()) // Symbol(i)
console.log(Boolean(i)) // true

注意:symbol值不可以和其他类型的值进行运算

Symbol属性名的遍历

使用 Symbol 类型值作为属性名,这个属性不会被 for…in 遍历到,也不会被 Object.keys() Object.getOwnPropertyNames() JSON.stringify() 获取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const name = Symbol('name')
const person = {
[name]: '老王',
age: 30,
sex: '男'
}
let for_key = []
for (let key in person) {
for_key.push(key)
}
console.log(for_key) // [ 'age', 'sex' ]
console.log(Object.keys(person)) // [ 'age', 'sex' ]
console.log(Object.getOwnPropertyNames(person)) // [ 'age', 'sex' ]
console.log(JSON.stringify(person)) // {"age":30,"sex":"男"}

使用 Object.getOwnPropertySymbols() 方法获取对象的所有symbol类型的属性名。

1
2
3
4
5
6
7
const name = Symbol('name')
const person = {
[name]: '老王',
age: 30,
sex: '男'
}
console.log(Object.getOwnPropertySymbols(person)) // [ Symbol(name) ]

可以用 ES6 新提供的 Reflect 对象 的静态方法 Reflect.ownKeys,它可以返回所有类型的属性名,所以 Symbol 类型的也会返回。

1
2
3
4
5
6
7
const name = Symbol('name')
const person = {
[name]: '老王',
age: 30,
sex: '男'
}
console.log(Reflect.ownKeys(person)) // [ 'age', 'sex', Symbol(name) ]

众所周知的Symbols

  1. Symbol.hasInstance
    方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。

    1
    2
    3
    4
    5
    6
    7
    // 当其他对象使用 instanceof 判断是否为这个对象的实例时,会调用你定义的这个方法,参数是其他的这个对象
    const obj01 = {
    [Symbol.hasInstance](other: any) {
    console.log(other) // { a: 'a' }
    }
    }
    console.log({ a: 'a' } instanceof (obj01 as any)) // false
  2. Symbol.isConcatSpreadable
    布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。

    1
    2
    3
    4
    5
    6
    7
    8
    let arr01 = [1, 2]
    console.log(arr01.concat([3, 4])) // [ 1, 2, 3, 4 ]
    let arr02 = ['a', 'b'] as any
    arr02[Symbol.isConcatSpreadable] = false
    console.log(arr02.concat(['c', 'd'])) // [ [ 'a', 'b', [Symbol(Symbol.isConcatSpreadable)]: false ], 'c', 'd' ]
    // 当数组arr02的 Symbol.isConcatSpreadable 设为 true 时,这个数组在数组的 concat 方法中不会被扁平化
    arr02[Symbol.isConcatSpreadable] = true
    console.log(arr02.concat(['e', 'f'])) // [ 'a', 'b', 'e', 'f' ]
  3. Symbol.iterator
    方法,被for-of语句调用。返回对象的默认迭代器。

    1
    2
    3
    4
    5
    const arr = [1, 2]
    const iterator = arr[Symbol.iterator]()
    console.log(iterator.next()) // { value: 1, done: false }
    console.log(iterator.next()) // { value: 2, done: false }
    console.log(iterator.next()) // { value: undefined, done: true }
  4. Symbol.match
    方法,被String.prototype.match调用。正则表达式用来匹配字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const obj = {
    [Symbol.match](val: any) {
    return val.length
    }
    }
    console.log('abcdefg'.match(obj)) // 7

    // 正则表达式用来匹配字符串
    const str: any = /abcdefg/
    str[Symbol.match] = false
    console.log('/abc/'.startsWith(str)) // false
    console.log('/abcdefg/'.endsWith(str)) // true
  5. Symbol.replace
    方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class obj02 {
    value: string
    constructor(value: any) {
    this.value = value
    }
    [Symbol.replace](str: any) {
    return `${str}:${this.value}`
    }
    }
    const n: any = '姓名'
    console.log(n.replace(new obj02('老王'))) // 姓名:老王
  6. Symbol.search
    方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。

  7. Symbol.species
    函数值,为一个构造函数。用来创建派生对象。

  8. Symbol.split
    方法,被String.prototype.split调用。正则表达式来用分割字符串。

  9. Symbol.toPrimitive
    方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。

  10. Symbol.toStringTag
    方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。

  11. Symbol.unscopables
    对象,它自己拥有的属性会被with作用域排除在外。

TypeScript 使用 never 关键字来表示逻辑上不应该发生的情况和控制流,即不应该存在的状态。

可能出现 never 的情况

  1. 函数抛出一个错误异常

    1
    2
    3
    const error = () => {
    throw new Error('error')
    }
  2. 函数包含一个无限循环

    1
    2
    3
    const loop = () => {
    while(true) {}
    }
  3. 类型判断或类型缩小的细化(全面性检查)

    在条件语句中,如果 TypeScript 能够推断出某个分支永远不会执行,那么该分支的类型会被推断为 never

    1
    2
    3
    4
    5
    6
    7
    const isTrue = (val: 'A' | 'B') => {
    switch(val) {
    case 'A': break;
    case 'B': break;
    default: const isFalse: never = val
    }
    }

never 和 void 的区别

  1. 如果没有为函数指定返回类型,并且在代码中没有返回任何内容,TypeScript 将推断其返回类型为 void。在TypeScript中,不返回任何内容的 void 函数实际上返回的是 undefined。

    void 类型的值可以是 undefinednull

    1
    2
    const fn = () => {}
    console.log(fn()) // undefined
  2. 不能将 void 指定给 never

    1
    2
    const fn = () => {}    // const fn: () => void
    let aa: never = fn() // 不能将类型“void”分配给类型“never”

never 类型的特点

  1. never 类型会从联合类型中移除

    1
    2
    3
    4
    5
    6
    type a = unknown | never  // unknown
    type b = any | never // any
    type c = number | never // number
    type d = string | never // string
    type e = object | never // object
    type f = boolean | never // boolean
  2. never 类型与任意类型的交叉类型都是 never

    1
    2
    3
    4
    5
    6
    type g = unknown & never  // never
    type h = any & never // never
    type i = number & never // never
    type j = string & never // never
    type k = object & never // never
    type l = boolean & never // never
  3. never 可以赋值给任意类型

    1
    2
    3
    4
    5
    6
    7
    let m: never
    let n: unknown = m
    let o: any = m
    let p: number = m
    let q: string = m
    let r: object = m
    let s: boolean = m
  4. 其他类型不能赋值给 never

    1
    2
    3
    4
    let t: never = 'a'      // 不能将类型“string”分配给类型“never”
    let u: never = 123 // 不能将类型“number”分配给类型“never”
    let v: never = { a: 1 } // 不能将类型“{ a: number; }”分配给类型“never”
    let w: never = true // 不能将类型“boolean”分配给类型“never”

枚举是 TypeScript 的少数功能之一,它不是 JavaScript 的类型级扩展。枚举允许开发者定义一组命名常量。使用枚举可以更轻松地记录意图,或创建一组不同的案例。TypeScript 提供基于数字和基于字符串的枚举。

typescript 枚举

数字枚举

先看例子

1
2
3
4
5
6
7
8
enum Type01 {
red,
green,
blue
}
console.log(Type01.red) // 0
console.log(Type01.green) // 1
console.log(Type01.blue) // 2

从例子可以看出,枚举中的每一个组员默认是从 0 开始的,如果我们希望从 1 开始,可以这样写

1
2
3
4
5
6
7
8
enum Type02 {
red = 1,
green,
blue
}
console.log(Type02.red) // 1
console.log(Type02.green) // 2
console.log(Type02.blue) // 3

或者这样写

1
2
3
4
5
6
7
8
enum Type03 {
red,
green = 3,
blue
}
console.log(Type03.red) // 0
console.log(Type03.green) // 3
console.log(Type03.blue) // 4

如上,我们定义了 Type02Type03 两个枚举,发现我们不论初始化 red 还是 green,余下的成员都是自动递增的。

字符串枚举

1
2
3
4
5
6
7
8
enum Type04 {
red = 'red',
green = 'green',
blue = 'blue'
}
console.log(Type04.red) // red
console.log(Type04.green) // green
console.log(Type04.blue) // blue

字符串枚举没有自增长的行为,字符串枚举可以很好的序列化,字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。。
那么,如果我们只初始化 green 会怎么样?

1
2
3
4
5
6
7
enum Type05 {
red, // 正常
green = 'green',
blue // 枚举成员必须具有初始化表达式
}
console.log(Type05.red) // 0
console.log(Type05.green) // green

如上,我们可以知道,没有初始化器的枚举要么放在初始化器枚举的前面,要么必须在使用数字常量或其他常量枚举成员初始化的数字枚举之后。

异构枚举

枚举可以混合字符串和数字成员

1
2
3
4
5
6
enum Type06 {
no = 0,
yes = 'yes'
}
console.log(Type06.no) // 0
console.log(Type06.yes) // yes

接口枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Type07 {
name = '老王',
sex = '男'
}
interface Person {
name: Type07.name,
sex: Type07.sex
}
let person: Person = {
name: Type07.name,
sex: Type07.sex
}
console.log(person.name) // 老王
console.log(person.sex) // 男

计算成员和常量成员

常量成员: 每个枚举成员都有一个与之关联的值 ,可以是常量或计算值。在以下情况下,枚举成员被认为是常量。

1
2
3
4
5
6
7
8
9
// 如下,Type08 中的第一个成员 name 没有初始化,在这种情况下,它被赋值为 0,而第二个成员sex的值是第一个成员name的值加1。
enum Type08 {
name,
sex,
age = 18
}
console.log(Type08.name) // 0
console.log(Type08.sex) // 1
console.log(Type08.age) // 18

const枚举

为了避免在访问枚举值时支付 额外生成的代码额外的间接成本,可以使用 const 枚举。

1
2
3
4
5
6
7
8
9
10
11
const enum Type09 {
red,
green,
blue
}
const color = [
Type09.red,
Type09.green,
Type09.blue
]
console.log(color) // [ 0, 1, 2 ]

const 声明和 普通声明的区别

1
2
3
4
5
6
7
8
// const 声明
const enum Type10 {
name = '老王'
}
console.log(Type10.name)

// 编译后
console.log("\u8001\u738B" /* Type10.name */);
1
2
3
4
5
6
7
8
9
10
// 普通声明
enum Type11 {
name = '老王'
}

// 编译后
var Type11;
(function (Type11) {
Type11["name"] = "\u8001\u738B";
})(Type11 || (Type11 = {}));

从上面的例子可以看出:

  1. const 声明的枚举会被编译成常量
  2. 普通声明的枚举编译完后是个对象

反向映射

除了为成员创建具有属性名称的对象外,数字枚举成员还获得从枚举值到枚举名称的反向映射。它包含了正向映射( name -> value)和反向映射( value -> name)

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Type12 {
name
}
const n = Type12.name
console.log(Type12[n]) // name

// 编译后
var Type12;
(function (Type12) {
Type12[Type12["name"] = 0] = "name";
})(Type12 || (Type12 = {}));
var n = Type12.name;
console.log(Type12[n]); // name

注意:字符串枚举成员根本不会生成反向映射。

1
2
3
4
5
6
7
// 元素隐式具有 "any" 类型,因为类型为 "Type13.name" 的表达式不能用于索引类型 "typeof Type13"。
类型“typeof Type13”上不存在属性“[Type13.name]”。
enum Type13 {
name = '张三'
}
const na = Type13.name
console.log(Type13[na]) // 报错

运行时的枚举

枚举是运行时存在的真实对象。

1
2
3
4
5
6
7
8
9
enum Type14 {
A,
B,
C
}
function fn(obj: { A: number }) {
console.log(obj.A) // 0
}
fn(Type14)

枚举成员的特性

成员只读,无法修改

1
2
3
4
5
enum Type15 {
A,
B
}
Type15.A = 1 // 无法为“A”赋值,因为它是只读属性

可以作为单独的类型存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Type16 {
A = 1,
B = 2
}
let C: Type16 = 3 // 已声明“C”,但从未读取其值,不能将类型“3”分配给类型“Type16”

// 解决方法
enum Type16 {
A = 1,
B = 2
}
let C: Type16 = 2

// 或者
enum Type16 {
A = 1,
B = 2,
C = 3
}
let C: Type16 = 3

数字枚举 value 的类型

1
2
3
4
5
6
7
8
9
enum Type17 {
A, // 默认值
B = 2, // 常量
C = 3 + 4, // 常量表达式
D = 'abc'.length, // 非常量表达式
E = Math.random(), // 随机数
F, // 枚举成员必须具有初始化表达式
G // 枚举成员必须具有初始化表达式
}

如上,在使用多种类型时,编辑器无法判断最后两个枚举成员 FG 应该使用哪种类型的值,所以需要为它们设置初始值。

1
2
3
4
5
6
7
8
9
enum Type17 {
A, // 默认值
B = 2, // 常量
C = 3 + 4, // 常量表达式
D = 'abc'.length, // 非常量表达式
E = Math.random(), // 随机数
F = 0,
G
}

使用枚举的好处

提高代码的可读性

枚举通过为一组相关的数值提供有意义的名字,使得代码更易于理解和阅读。比如每个星期的7天。

1
2
3
4
5
6
7
8
9
enum Week{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}

类型安全

从枚举的特性我们可以知道,枚举和枚举成员可以作为单独的类型存在,这意味着无法将不属于该枚举的值赋值给枚举类型的变量。这在编译时提供了错误检查层,避免了不必要的错误,提高了类型安全。

易于维护

如果你的代码依赖于一组特定的数值,而这些数值可能会在项目的生命周期中改变,使用枚举可以使得这些改变更容易管理。当你需要修改或添加新的值时,只需要更新枚举的定义即可,而不需要搜索和替换整个代码库中的硬编码值。如下,只需要直接修改 People 中的 nameage 即可。

1
2
3
4
5
6
enum People {
name = '小红',
age = 18
}
console.log(People.name)
console.log(People.age)

方便调试

在调试过程中,看到一个具有描述性名称的枚举值比看到一个可能没有任何含义的数值更有帮助。比如每个星期的7天。

1
2
3
4
5
6
7
8
9
enum Week{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}

提高性能

枚举值通常在编译时解析,因此在运行时没有额外的成本。

元组是一种特殊的数组,元组种元素的类型可以不相同,数组中元素的类型必须相同。

相同点

  1. 元组和数组都支持索引,可以通过索引访问元素。
    1
    2
    3
    4
    let arr_06: number[] = [1, 2]
    console.log(arr_06[0]) // 1
    let tuple_03: [string, number] = [ '小红', 29 ]
    console.log(tuple_03[0]) // 小红
  2. 元组和数组都有一个名为length的长度属性,可以用来获取当前元素的个数。
    1
    2
    3
    4
    let arr_06: number[] = [1, 2]
    console.log(arr_06.length) // 2
    let tuple_03: [string, number] = [ '小红', 29 ]
    console.log(tuple_03.length) // 2

不同点

定义

  1. 数组是一种数据结构,用于存储一系列相同类型的元素。它可以容纳多个值,并通过索引来访问和操作这些值。
    1
    2
    3
    4
    5
    let arr_01: number[] = [1, 2, 3]
    let arr_02: Array<string> = [ 'a', 'b', 'c' ]
    // 固定数组的长度
    let arr_03: Array<number> = new Array(3)
    let arr_04 = new Array<number>(3)
  2. 元组类型是另一种数组类型(可以简单理解为元组是数组的变种),它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。
    1
    let tuple_01: [ string, number ] = [ '小翠', 18 ]

可变性

  1. 数组是可变的,可以直接修改数组的现有元素。
    1
    2
    3
    let arr_05 = [1, 2]
    arr_05.push(3)
    console.log(arr_05[2]) // 3
  2. 元组是不可变的,如果修改元组会导致创建新的元组副本。
    1
    2
    3
    4
    let tuple_02: [ string, number ] = [ '小红', 29 ]
    tuple_02.push('女')
    // 长度为 "2" 的元组类型 "[string, number]" 在索引 "2" 处没有元素。
    console.log(tuple_02[2])

性能

  1. 数组的每次修改都需要重新分配内存,这可能导致性能上的开销。
  2. 由于元组是不可变的,它们可能在某些情况下被视为性能更优的选择,尤其是在频繁的迭代和修改操作时。

用途

  1. 数组则常用于存储一组相同类型的数据,以及进行数学计算和数据处理。
    1
    2
    // 必须全部是number类型;
    let arr_01: number[] = [1, 2, 3]
  2. 元组通常用于存储不同类型的数据,或者作为函数的参数,因为它可以安全地传递给函数并保持不变。
    1
    2
    // [ 姓名, 年龄 ]
    let tuple_01: [ string, number ] = [ '小翠', 18 ]

元组类型是另一种 Array 类型,它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型。

简单使用

1
2
type Tuple = [ string, number ]
const person_01: Tuple = [ '张三', 24 ]

这里的 person 是 string 和 number 的元组类型。元组类型可以把多个元素作为一个单元传递。
你也可以这样写

1
const person_02: [ string, number ] = [ '李四', 30 ]

赋值和访问

1
2
3
4
5
6
let person_03: [ string, number ] = [ '王五', 18 ]
person_03[0] = '老六'
person_03[1] = 25
console.log(person_03[0]) // 老六
console.log(person_03[1]) // 25
console.log(person_03) // [ '老六', 25 ]

在函数中的使用

1
2
3
4
5
6
7
8
let PersonFn = (person: [ string, number ]) => {
console.log(person[0]) // 小七
console.log(person[1]) // 20
// console.log(person[2])
console.log(person) // [ '小七', 20 ]
}
PersonFn([ '小七', 20 ])

如果我们试图索引超过元素的数量,我们会得到一个错误,比如

1
2
// 长度为 "2" 的元组类型 "[string, number]" 在索引 "2" 处没有元素。
console.log(person[2])

解构元组

使用 JavaScript 的数组解构来 解构元组

1
2
3
4
5
let TupleFn = (person: [ string, number ]) => {
const [name, age] = person
console.log(`我是${name},今年${age}岁`) // 我是老王,今年28岁
}
TupleFn([ '老王', 28 ])

可选属性

1
2
3
4
5
6
type Man = [string, number, string?]
function ManFn(person: Man) {
console.log(person.length)
}
ManFn(['老王', 40]) // 2
ManFn(['老王', 40, '男']) // 3

总结

元组类型在大量基于约定的 API 中很有用,其中每个元素的含义都是 “明确的”。这使我们在解构变量时可以灵活地命名变量。

1
2
3
let person: [string, number, string] = ['老王', 40, '男']
const [name, age, sex] = person
console.log(`我是${name},今年${age}岁,${sex}`) // 我是老王,今年40岁,男

类描述了所创建的对象共同的属性和方法。

最基本的类

1
2
3
4
5
6
7
8
9
// ts
class Person<T> {}

// 编译成js后
var Person = /** @class */ (function () {
function Person() {
}
return Person;
}());

简单使用

定义类的关键字为 class,后面紧跟类名,类可以包含以下几个模块(类的数据成员):

  1. 字段:字段是类里面声明的变量。字段表示对象的有关数据。
  2. 构造函数:类实例化时调用,可以为类的对象分配内存。
  3. 方法:方法为对象要执行的操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // ts
    class Person<T> {
    // 字段
    name: string
    // 构造函数
    constructor(name: string) {
    this.name = name
    }
    // 方法
    getName() {
    return this.name
    }
    }
    const person = new Person('老王')
    console.log(person.getName()) // 老王

    // 编译成js后
    var Person = /** @class */ (function () {
    // 构造函数
    function Person(name) {
    this.name = name;
    }
    // 方法
    Person.prototype.getName = function () {
    return this.name;
    };
    return Person;
    }());
    var person = new Person('老王');
    console.log(person.getName());

new 关键字

我们使用 new 关键字来实例化类的对象,类实例化时会调用构造函数,类中的字段属性和方法可以使用 . 号来访问。完整实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person<T> {
// 字段
name: string
// 构造函数
constructor(name: string) {
this.name = name
}
// 方法
getName() {
return 'getName: ' + this.name
}
}
// 创建一个对象
const person = new Person('老王')
// 访问字段
console.log(person.name) // 老王
// 访问方法
console.log(person.getName()) // getName: 老王

类的继承

顾名思义,子类继承父类后,可以使用父类的属性和方法。但是要注意的是,子类只能继承一个父类,TypeScript 不支持继承多个类,但支持多重继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person<T> {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
class Info extends Person<unknown> {
sex: string
constructor(name: string, age: number, sex: string) {
super(name, age)
this.sex = sex
}
getInfo() {
return `姓名:${this.name},年龄:${this.age},性别:${this.sex}`
}
}
const info = new Info('翠花', 18, '女')
console.log(info.getInfo()) // 姓名:翠花,年龄:18,性别:女

多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person<T> {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
class Like extends Person<unknown> {
mylike: string
constructor(name: string, age: number, mylike: string) {
super(name, age)
this.mylike = mylike
}
}
class Info extends Like {
sex: string
constructor(name: string, age: number, mylike: string, sex: string) {
super(name, age, mylike)
this.sex = sex
}
getInfo() {
return `姓名:${this.name},年龄:${this.age},性别:${this.sex},爱好:${this.mylike}`
}
}
const info = new Info('翠花', 18, '游泳', '女')
console.log(info.getInfo()) // 姓名:翠花,年龄:18,性别:女,爱好:游泳

继承类的方法重写

子类得到父类的属性和方法后,我们相当于继承了父类的财产,那么在没有任何约束的情况下,我们就可以按照自己的意愿来使用这笔财产,在遇到父类和我们在使用这笔财产做法有出入时,我们便可以按照自己的方法来操作。这个过程称之为方法的重写。

1
2
3
4
5
6
7
8
9
10
11
12
class Father<T> {
loveFn() {
console.log('我是父类,我爱喝红牛')
}
}
class Son extends Father<unknown> {
loveFn() {
console.log('我是子类,我爱喝饮料')
}
}
const son = new Son()
son.loveFn() // 我是子类,我爱喝饮料

如果我们想要引用父类的属性和方法,可以使用 super 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Father<T> {
loveFn() {
console.log('我是父类,我爱喝红牛')
}
}
class Son extends Father<unknown> {
loveFn() {
super.loveFn()
console.log('我是子类,我爱喝饮料')
}
}
const son = new Son()
son.loveFn() // 我是父类,我爱喝红牛 我是子类,我爱喝饮料

static 关键字

static 关键字用于定义类的数据成员(属性和方法)为静态的,静态成员可以直接通过类名调用。

1
2
3
4
5
6
7
8
class Person<T> {
static person_name: string
static getName() {
return Person.person_name
}
}
Person.person_name = '翠花'
console.log(Person.getName()) // 翠花

instanceof 运算符

instanceof 运算符用于判断对象是否是指定的类型,如果是返回 true,否则返回 false。

1
2
3
class Person<T> {}
const person = new Person()
console.log(person instanceof Person) // true

访问控制修饰符

TypeScript 支持 3 种不同的访问权限:

  1. public(默认):公有,可以在任何地方被访问。
  2. protected:受保护,可以被其自身以及其子类访问。
  3. private:私有,只能被其定义所在的类访问。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Person<T> {
    public name: string = '老王'
    private age: number = 30
    protected sex: string = '男'
    getAge() {
    return this.age
    }
    }
    class Sex extends Person<unknown> {
    getSex() {
    return this.sex
    }
    }
    const person = new Person()
    console.log(person.name) // 老王
    // console.log(person.age) // 报错,私有属性,只能在Person类中访问
    console.log(person.getAge()) // 30
    // console.log(person.sex) // 报错,保护属性,只能在Person类或Sex中访问
    const sex = new Sex()
    console.log(sex.getSex()) //男

类和接口

类使用关键字 implements 实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Info {
name: string,
age: number
}
class Person implements Info {
name: string
age: number
sex: string
constructor(name: string, age: number, sex: string) {
this.name = name
this.age = age
this.sex = sex
}
getInfo() {
return `姓名:${this.name},年龄:${this.age},性别:${this.sex}`
}
}
const person = new Person('翠花', 18, '女')
console.log(person.getInfo()) // 姓名:翠花,年龄:18,性别:女

获取器/设置器

如果 get 存在但没有 set,则属性自动为 readonly
如果不指定 setter 参数的类型,则从 getter 的返回类型推断
getter 和 setter 必须有相同的 成员可见性

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
_name: string
get name() {
return this._name
}
set name(n:string) {
this._name = n
}
}
const person = new Person()
person.name = '老王'
console.log(person._name) // 老王

static类中的块

静态块允许你编写具有自己作用域的语句序列,这些语句可以访问包含类中的私有字段。这意味着我们可以编写具有编写语句的所有功能的初始化代码,不会泄漏变量,并且可以完全访问我们类的内部结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
static _name: string
get name() {
return Person._name
}
set name(n: string) {
Person._name = n
}
static {
console.log('static类中的块')
Person._name = '老王'
}
}
const person = new Person()
console.log(person.name) // 老王
person.name = '翠花'
console.log(person.name) // 翠花

抽象类

在TypeScript中,可以使用 abstract 关键字来定义抽象类和抽象方法。抽象类不能被直接实例化,它只能被用作其他类的基类。抽象方法是没有实现的方法,必须在子类中被重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 抽象类
abstract class A {
// 抽象属性
abstract name: string
// 抽象方法
abstract getName(): void
// 具体方法
setName(name: string) {
this.name = name
}
}
class B extends A {
name: string
constructor(name: string) {
super()
this.name = name
}
getName(): string {
return this.name
}
}
const b = new B('老王')
console.log(b.getName()) // 老王
b.setName('翠花')
console.log(b.getName()) // 翠花

小技巧

在以上方法中我们一直都使用构造函数初始化,如果我们不想在构造函数中初始化,可以找到 typescript 的配置文件 tsconfig.json 中的 strictPropertyInitialization,将其改为 false 即可。

1
2
3
4
class Person {
name: string
age: number
}

共同点

都可以用来描述对象(Object)和函数(Function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类型别名
type A_Type = {
name: string,
age: number
}
type B_Type = (args: boolean) => void
const a:B_Type = (args: boolean) => args

//接口类型
interface C_Interface {
name: string,
age: number
}
interface D_Interface {
(args: boolean): void
}
const b:D_Interface = (args: boolean) => args

都可以进行继承(扩展)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
type A_Type = {
name: string,
}
type B_Type = {
age: number
}
interface C_Interface {
sex: string,
}
interface D_Interface {
phone: number | string
}

// type 继承 type
type Type_Extends_Type = A_Type & B_Type & { adrress: string }
const a: Type_Extends_Type = {
name: '翠花',
age: 18,
adrress: '中国'
}

// interface 继承 interface
interface Interface_Extends_Interface extends C_Interface, D_Interface {
job: string
}
const b: Interface_Extends_Interface = {
sex: '女',
phone: 13222222222,
job: '程序员'
}

// type 继承 interface
type Type_Extends_Interface = A_Type & C_Interface
const c: Type_Extends_Interface = {
name: '翠花',
sex: '女',
}

// interface 继承 type
interface Interface_Extends_Type extends B_Type, D_Interface {}
const d: Interface_Extends_Type = {
age: 18,
phone: 13222222222,
}

都可以实现(implements)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// interface
interface IDog {
setNameFn(name: string): void
}
class Dog implements IDog {
setNameFn(name: string): void {
console.log('取名成功')
}
}

// type
type ICat = {
setNameFn: (name: string) => void
}
class Cat implements ICat {
setNameFn(name: string): void {
console.log('取名成功')
}
}

都不会出现在编译结果里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ts
type AA = {
name: string
}
interface BB {
age: number
}
const cc: AA = {
name: '老王'
}
const dd: BB = {
age: 30
}

// 编译成js后
var cc = {
name: '老王'
};
var dd = {
age: 30
};

不同点

语法不同,type需要等号,interface不需要。

interface定义的是对象的结构,而非单独的类型。

1
2
3
4
5
6
type PersonType = {
name: string
}
interface PersonInterface {
name: string
}

type可以用来为任何类型(包括原始类型、函数、对象等)创建别名。

1
2
type A = string
type b = number

type可以声明联合类型。

1
2
3
4
5
type C = string | number
type D = { name: string } | { age: number }
const e: D = { name: '老王' }
const f: D = { age: 30 }
const g: D = { name: '老王', age: 30 }

type可以声明元组类型。

1
2
type H = [ string, number, boolean ]
const i: H = [ '老王', 30, true ]

interface 多次声明一个同名的接口可以合并

类型别名不能重复定义相同名字,会直接报错

1
2
3
4
5
6
7
8
9
10
interface Info_Interface {
name: string
}
interface Info_Interface {
age: number
}
const j: Info_Interface = {
name: '老王',
age: 30
}

扩展的方式不一样

接口使用 extends 关键字来进行扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface K {
name: string
}
interface L {
age: number
}
interface M extends K, L {
sex: string
}
const n: M = {
name: '老王',
age: 30,
sex: '男'
}
console.log(n) // { name: '老王', age: 30, sex: '男' }

类型别名使用 & 符号进行扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type O = {
name: string
}
type P = {
age: number
}
type Q = {
sex: string
} & O & P
const r: Q = {
name: '老王',
age: 30,
sex: '男'
}
console.log(r) // { name: '老王', age: 30, sex: '男' }

类型别名可以使用typeof获取实例的类型进行赋值

1
2
const div = document.createElement('div')
type S = typeof div // HTMLDivElement

类型别名可以使用in来实现类型映射

1
2
3
4
5
6
7
8
9
type Keys = 'name' | 'age'
type T = {
[ key in Keys ]: string | number
}
const u: T = {
name: '老王',
age: 30
}
console.log(u) // { name: '老王', age: 30 }

总结

官方推荐用 interface,其他无法满足需求的情况下用 type。但其实,因为 联合类型交叉类型 是很常用的,所以避免不了大量使用 type 的场景,一些复杂类型也需要通过组装后形成类型别名来使用。

在TypeScript中,我们可以使用 type 关键字来定义一个类型的别名,它可以为复杂的类型结构创建更具描述性和易于理解的新名字,这有助于提高代码的可读性和可维护性,类型别名常用于联合类型

基本用法

1
2
type num = number
const a: num = 1

联合类型别名(使用最多)

1
2
3
4
type info = number | string
const x: info = 2
const y: info = 'info'
const z: info = true // 报错,类型别名info并没有布尔类型对象类型别名

对象类型别名

1
2
3
4
5
6
7
8
9
type obj = {
name: string,
age: number
}
const b: obj = {
name: '老王',
age: 18
}
console.log(b) // { name: '老王', age: 18 }

函数类型别名

1
2
3
type fn = (name: string) => string
const c:fn = (name: string) => name
console.log(c('翠花')) // 翠花

元组类型

1
2
3
type arr = [ string, number ]
const data: arr = [ '老王', 18 ]
console.log(data) // [ '老王', 18 ]

复杂类型结构

使用交叉类型和联合类型来组合多个类型。

1
2
3
4
5
6
7
8
9
10
11
12
type Person = {
name: string,
age: number
} & {
sex: string
}
const person = {
name: '翠花',
age: 18,
sex: '女'
}
console.log(person) // { name: '翠花', age: 18, sex: '女' }