区域注释
TypeScript 可以添加区域注释,可以让VS Code等编辑器识别为一个代码区域,区域注释使用的是单行注释语法:
1 | //#region 区域描述 |
运算符
空值合并运算符
1 | a ?? b |
如上,当 a
为 undefined
或 null
的时候,返回 b
。相比于 a || b
,当 a
为 false
或空字符串时,a ?? b
也返回a,这在某些对于 false
或空字符串也起作用的场景,空值合并运算符是非常有用的。
TypeScript的原始类型
TypeScript常见原始类型有:
- boolean
- string
- number
- bigint
- symbol
- undefined
- null
- void
- 枚举类型
- 字面量类型
这里的原始类型都是小写的,如下:
1 | const a: bigint = 1n; |
symbol
symbol对应JavaScript原始类型的Symbol。考虑如下代码:
1 | let s1: symbol = Symbol(); |
Symbol
表示不变的,但是上述 symbol
类型的 s1
却可以再次赋值为其他 Symbol
值,这就导致在定义接口 A
的时候使用的 s1
可以变化,这就违背了 Symbol
不变的性质。为了避免这种问题,TS引入了 unique symbol
类型,该类型的 Symbol
必须用 const
申明( let
或 var
声明直接报错),这样就不能再修改了:
1 | const s1: unique symbol = Symbol(); |
unique symbol
只能通过 Symbol()
或者 Symbol.for()
赋值,但symbol
类型没有这样的限制。
1 | const s1: unique symbol = Symbol(); |
枚举类型
TS中的枚举实际上会编译为对象:
1 | enum Direction { |
可以看出枚举实际上是编译为对象,而且值实际上是数值,如上面 Direction.UP === 0
。当然因为这里也对数值赋值为对应的字符串,所以 Direction[0] === 'UP'
。
这里也可以给某个枚举值赋值为数字,这样枚举值的计数就会从赋值开始:
1 | enum Direction { |
可以看到 UP
是 -2
,DOWN
是 -2 + 1
也就是 -1
;LEFT
是 10
,RIGHT
是 10 + 1
也就是 11
。
当然枚举也可以是字符串:
1 | enum Direction { |
const 枚举
指的是用 const
声明的枚举,const 枚举
编译跟普通枚举不同,它编译后的结果是在使用的地方直接替换为对应的字符串或数字:
1 | const enum Direction { |
strictNullChecks
当给一个类型的值设置为 null
或者 undefined
的时候默认并不会报错:
1 | const s: string = undefined; // OK |
但是可以通过配置 --strictNullChecks
来严格检查是否为空,配置方式有两种:
- 运行命令
tsc index.ts --strictNullChecks false
- 在tsconfig.json中添加:
1 | { |
注:大多数配置参数都是通过上述两种方式配置的, 但一般使用TS时需要编辑器直接告诉我们哪里编码错误,而不是等编译的时候再检查,所以这里更推荐第二种方式。更多配置请参考这里。
配置以后上面的情况如下:
1 | const s: string = undefined; // Error |
注:
any
和unknown
仍然可以设置为null
和undefined
。
顶端类型
顶端类型是一种通用类型,有时也称为通用超类型。在类型系统中,所有类型都是顶端类型的子类。
TypeScript中有两种顶端类型:
- any
- unknown
any
类型允许执行任意操作而不会产生编译错误(但运行时候也可能出现错误),通常用于跳过类型检查:
1 | const a: any = 0; |
对于一个方法来说如果没有声明类型则默认是any
类型,可以通过 --noImplicitAny
参数来控制不允许隐式设置any类型。
1 | // --noImplicitAny: false |
--noImplicitAny
的配置方法跟上面 --strictNullChecks
配置方法类似,如修改 tsconfig.json
文件:
1 | { |
unknown
与 any
类型任何其他类型都可以赋值给 unknown
,但是unknown
类型的值只能赋值给 unkonwn
和 any
,而且 unknown
不允许执行绝大多数的操作:
1 | let a: unknown = 0; |
通常使用 unknown
需要自行判断类型:
1 | function (x: unknown) { |
尾端类型
尾端类型是所有类型的子类型,它只有一个类型就是 never
,该类型甚至没有值。由于它是所有类型的子类型,所以它可以赋值给任何类型,但是其他类型都不能赋值给它,包括 any
。
1 | let a: never; // OK |
注:虽然
never
可以赋值给任何类型,但是如果在--strictNullChecks
为true
的时候,同样会报错。如上第二行,在--strictNullChecks
为true
时,也是会报错的。
neber
的使用场景:
- 函数没有返回值。
1 | function fn(): never { |
- 函数死循环。
1 | function fn(): never { |
- 有些条件类型判断中会使用到never。
1 | type Exclude<T, U> = T extends U ? never : T; |
只读数组
数组的表示方法:
1 | const a: number[] = [1, 2, 3]; |
只读数组的表示方法:
1 | const a: ReadonlyArray<number> = [1, 2, 3]; |
元组类型
元组类型是数组类型的子类型,值是一个数组。元组一般是长度固定的数组,相比较数组每个元素都是相同的类型,元组每个元素的类型都可以不同。由于元组类型是数组的子类型所以元组类型可以赋值给数组类型,前提是元组中的每一项都符合数组的每一项类型;数组类型是不能赋值给元组类型的。
1 | const a: [number, string] = [1, '2']; // a就是元组类型 |
对象类型
Object
在TypeScript中值 Object
(window.Object)的类型并不是 Object
类型,而是 ObjectConstructor
类型。通过调用new Object()
获取到的值的类型才是Object
类型。
1 | interface Object { |
Object
类型的值除了 null
和 undefined
外,其他任何值都可以赋值。为什么相如 boolean
这种原始数据类型也能赋值给 Object
呢?因为原始类型会自动拆箱和装箱啊。但是声明的 Object
类型的值不能调用 window.Object
以外定义的属性和方法。
1 | const a: Object = new Object(); // OK |
object
object
相比较于 Object
更加严格,只能是对象类型,而不能是 boolean
这样的原始数据类型,同样的也只能调用 Object
类型定义的属性和方法。
1 | const a: object = new Object(); // OK |
对象字面量类型
一看就会的对象字面量类型:
1 | const a: { x: number, y: number} = { |
上面类型 { x: number, y: number}
就是对象字面量类型,是不是很简单?看一个稍微复杂一点的例子:
1 | const a: 'a' = 'a'; // 注意这里的类型使用了 'a' 而不是string, 如果是string则不能用在对象中作为属性 |
上面 obj.e
由于没有出现在类型定义中所以报错了,可以通过如下方式添加多余参数的定义:
1 | let obj: { |
上面 [prop: string]: any
表面属性值可以是任何类型的,所以不会报错,想想上面的 obj.a
,其中 a
相当于也是一个 string
类型,如果修改为[prop: string]: string
,而 a
的类型是 boolean
,那么就会存在 boolean
和 string
冲突,所以就会报错。
函数类型
函数的参数可以是剩余参数,剩余参数类型可以是数组或元组:
1 | // 剩余参数是数组 |
通常在定义函数就已经确定好函数的类型了,但是你也可以给一个变量设置为函数的类型,这里有两种方式:
1 | // 函数的调用签名定义: |
类本质上是函数,类的签名可以用构造函数来表示,格式如下:
1 | // 类的构造签名定义 |
构造签名和调用签名可以共存,如下:
1 | type Str = { |
上述类型 Str
可以通过 new
来创建 String
对象,也可以通过函数调用返回 string
类型;实际上 String
函数就属于这种类型。
函数重载
函数重载是指一个函数有多个同名的函数签名,如下:
1 | function add(x: number, y: number): number; |
不带有函数体的函数声明语句叫做函数重载,它只提供函数的类型信息。重载函数由一条或多条函数重载语句以及一条函数实现语句构成。只有一条重载语句跟函数签名是对应的函数重载,是允许的,但通常没啥意义(一条的时候函数重载可以省略)。对于多条函数重载来说,每个函数重载中的函数名和函数实现中的函数名必须一致。同时函数重载语句与其他函数重载语句或函数实现语句之间不能出现其他语句,否则将产生编译错误。函数重载语句在函数编译后将会删除。在上述例子中,如果没有函数重载,只看函数实现则可以出现x
是nunber
,y
是string
这种情况,但是函数重载限制了这种情况。需要注意的是函数实现必须兼容所有的重载语句。
函数重载也可以通过对象字面量来表示,如下:
1 | const add: { |
需要注意的是函数字面量相当于是先定义了add的类型,然后再给实现,所以实现的参数和返回类型一定要满足定义中的所有情况,上述实现中x
使用了any
类型,如果是number | string
,则不符合定义函数中的任意一项,所以也会报错。
函数重载解析顺序
当一个函数的实际参数数量不少于函数重载中的必须参数且不多于重载函数中定义的所以参数数量,同时实际参数的类型能够匹配函数重载中的参数,则认为这条函数重载符合函数定义,如果有多条符合的则从上到下解析。如下
1 | function f(x: any): string; |
上述第一条函数重载和第二条函数重载都满足函数调用的参数,根据从上倒下应该选中第一条函数重载,该函数重载返回的是string
类型,而不是0
,所以报错。因此,一般写函数重载的时候我们应先定义范围更小的函数重载。
函数中的this类型
通常我们在函数中使用 this
是不会报错的,但是如果 --noImplicitThis=true
的时候,则会报错。
1 | function foo(bar: string) { |
可以在函数定义的时候第一个参数定义 this
的类型,但调用的时候需要调用call
,bind
,apply
等来调用。
1 | function foo(this: { bar: string }, bar: string): any { |
接口
接口可以定义任意对象类型,但无法表示原始类型。接口类型的成员可以是属性签名、调用签名、构造签名、方法签名和索引签名。另外接口可以多继承。通过一个例子看接口的各种用法:
1 | interface TestInterface { |
类型别名
类型别名相当于给已有类型起了一个别名,它不会创建类型,但是可以给任意类型起别名。
1 | type DecimalDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; |
类型别名与接口的比较:
- 类型别名能够表示非对象类型,而接口不行。
- 接口可以继承其他接口、类等对象类型。而类型别名不能继承(但可以通过交叉类型
&
来实现类似的功能); - 错误提醒对类型别名引用对应的类型,而接口引用接口名。
- 接口可以同名,同名接口对应的值会合并,但是类型别名不能同名。
类
TypeScript的类与JavaScript的类大多数语法都是类似的,但TypeScript对类的一下功能做了扩充,如接口实现、泛型类等。一个简单的示例:
1 | interface IA { |
类的可访问性
访问修饰符:
public
(默认): 当前类的内部、外部以及派生类的内部均可访问,不写访问修饰符默认就是public
。protected
: 在当前类和派生类内部可以访问,不允许当前类外部(如创建的对象)访问。private
: 只有当前类的内部可以访问。
ES13类的私有字段TS也是支持的,私有字段仅有类内部可以访问:
1 | class A { |
参数成员
在类的构造函数的参数中使用访问修饰符或readonly修饰,则该参数自动成为类的成员变量,不需要在构造函数中使用 this.a = a;
这样的语句。
1 | class A { |