在 TypeScript 中,可以把类型理解成一系列值的集合,例如:
number:所有数字的集合
对象类型:内部所有属性的集合
顶层类型与底层类型
TypeScript 有两个顶层类型(any
和 unknown
),一个底层类型(never
)。
- 顶层类型:所有类型的并集,也可以说是所有类型的父集
- 底层类型:与顶层类型相反,可以说是所有类型的子集
类型兼容
TypeScript 中,当类型 A 在集合上属于类型 B 的父集时,他就可以赋值给类型 B,而反过来则会报错。
例如对于上文的顶层与底层类型,可以追加两个定义:
- 顶层类型: 任何类型都可以赋值给顶层类型,但顶层类型不能赋值给其他类型
- 底层类型: 与顶层类型相反,它可以赋值给任意类型,但其他类型都不能赋值给它
any
any
属于 TypeScript 的顶层类型,所以任何类型都可以赋值给他。
1 | let a: any = 10; |
但 any 有个很在 TypeScript 中独一无二的地方,即它也可以被赋值给任意类型(神一样的存在)
1 | let b: number = <any>"10"; // 不会报错 |
听我一句劝,不要写 AnyScript
自动推断
当一个类型,TypeScript 无法推断出他的具体类型时,就会被标记为 any
。
相对应的,TypeScript 提供了一个编译选项: noImplicitAny
,开启该选项时,只要自动推断出 any
,就会报错。
1 | // tsconfig.json |
但有一个特例,对于使用 let
和 var
定义的未设置初始值的变量,未对其设置类型时,会被自动推断出 any
,但此时不会报错。
1 | let a; // 不会报错 |
因为这个原因,建议在使用 let/var 定义变量时,如果没有初始值,一定要手动为其设置类型。
unknown
该类型代表任何值,属于严格意义上的 TypeScript 的顶层类型
1 | let a: unknown = 10; |
与 any 的不同点
unknown
不允许赋值给除 any
与自身的任何类型
1 | let a: number = <unknown>10; // 报错 |
类型为 unknown
的方法,不能直接调用
1 | let foo: unknown = () => {}; |
unknown
只能作有限的运算,分别为比较运算(===
、!==
、||
、&&
、?
)、取反(?
)、typeof
和 instanceof
1 | let a: unknown = 10; |
当使用 keyof 关键字对其进行类型提取时,与 any
的表现也不一样(keyof 关键字后文会讲):
1 | let any: keyof any; // any: string | number | symbol |
使用方式
unknown
的最佳使用方式是通过类型缩小来确定类型
1 | let a: unknown = 10; |
基本所有范围较大的类型的最终使用方式都是类型缩小,包括后面要提到的
object
、联合类型等
never
从集合论上讲,never
属于空集。它是所有类型的子集,是 TypeScript 唯一的底层类型。
因为是所有类型的子集,所以它可以被赋值给任意类型
1 | let a: number = <never>10; |
数据类型
TypeScript 继承了 JavaScript 的类型设计,除了自身新增的类型外,JavaScript 中的类型 TypeScript 也全部支持。
基本类型
与 JavaScript 相同,即:number
、string
、boolean
、bigint
、symbol
、null
、undefined
、object
下面仅针对部分较为特殊的做出说明。
boolean
boolean
类型其实就是 true
和 false
两个字面量类型的集合。
object
object
是对象、数组、函数的集合,但其只定义了通用对象的属性,不包括用户自定义的对象属性
1 | const obj: object = { a: 1 }; |
null 与 undefined
编译配置项 strictNullChecks
可以对 null
和 undefined
做出更严格的校验。当它设置为 false
时,存在以下情况:
- 对于使用
const
声明的未设置类型的变量,被赋值为null
或undefined
时,该变量的类型会被自动推断为any
null
和undefined
会像any
那样可以被赋值给任意类型,他俩也可以被互相赋值
为了避免出现问题,建议还是将 strictNullChecks
打开比较好。
包装对象类型
JavaScript 中的五个原始类型均有其对应的包装对象,为此,TypeScript 也提供了对应的包装类型,这些包装类型均为基本类型的大写方式:
- Boolean <–> boolean
- Number <–> number
- String <–> string
- BigInt <–> bigint
- Symbol <–> symbol
通过以下示例可以明确的看出包装类型与基本类型的区别
1 | let str = "mari"; // str: string |
其中由于 BigInt()
与 Symbol()
不允许作为构造函数使用,我们无法通过比较直接的方式得到包装类型。而且这两个类型的包装类型和基本类型在 TypeScript 中的没有区别,建议都使用小写的基本类型。
Object 类型
相比 object
,Object
表示的是一种广义上的对象类型,只要可以转换为对象的值(除了 null
和 undefined
),都属于 Object
类型,也就都可以赋值给 Object
类型。
{}
是 Object
类型的简写方式,下面的写法是等价的
1 | let obj1: Object; |
值类型
在 TypeScript 中,单个原始类型值也是一个类型
1 | let str: "mari"; |
值类型是一种非常底层的类型,在集合概念中为最小的集合。因此像下面这种赋值是都不被允许的
1 | let str: "mari" = "mari"; |
值类型与 const
使用 const
声明的原始类型值,会被自动推断为值类型。
1 | const str = "mari"; // str: "mari" |
表达式右边的结果不是一个确切的值时,不会推断为值类型
1 | const num = 10 + 8; // num: number |
很好理解,TypeScript 只编译,并不会进行语句执行,也就无法得到计算的结果。
这样就会导致出现一些奇怪的问题:
1 | const num: 18 = 10 + 8; // 报错: Type `number` is not assignable to type `18` |
联合类型
可以将多个类型组合成一个类型,每个类型使用 |
分割,这种新组成的类型叫做联合类型。联合类型可以理解成类型系统中的并集
1 | // number | string 就是一个联合类型,表示既可以是字符串也可以是数字 |
联合类型可以由任意类型组成,数量不限
1 | let name: "mari" | "marry" | "Marry Kozakura"; |
第一个类型前面也可以添加 |
,这是为了便于排版书写
1 | let name: |
子类型收敛
若组成联合类型中的类型存在父子关系,则结果中仅会存在父类型。例如下面示例的 "mari"
类型就被 string
给“吃掉了”。
1 | let name: "mari" | string | null; // name: string | null |
而 any
与 unknown
两个顶层类型联合时,会得到 any
类型,可以勉强按照 any
范围比 unknown
更大来理解(虽然这样不准确)。
交叉类型
通过 &
拼接,多个类型也可以组成一个交叉类型。交叉类型即是多个类型的共同点,可以理解为类型系统中的交集
1 | let name: string & number; // name: never |
上面的 name 示例中,由于 string
和 number
并不存在交集,所以结果为空集合 never
类型。
父类型收敛
与联合类型类似,交叉类型也存在类型收敛,结果中会仅保留子类型,父类型会被丢弃。
1 | let val1: 1 & number; // val1: 1 |
特殊情况: 除了 never
类型外, any
类型与任何类型交叉均为 any
类型
1 | let val1: number & any; // val1: any |
对象类型的交叉
因为需要使得交叉结果均可继承自交叉双方的类型,所以当对象交叉时,结果是这样的
1 | let obj: { name: string } & { age: number }; // obj: { name: string, age: number } |
当两个对象类型存在相同属性但类型不同时,将会对该属性进行交叉
1 | type Base1 = { |
函数类型的交叉
TypeScript 会利用函数重载的特性来实现不同函数的交叉运算
因此可能会出现如下这种情况
1 | type F1 = (arg1: string, arg2: string) => void |
且函数类型交叉运算后,相同参数变为联合类型,而返回值类型将做交叉运算
因此将会出现下面这种情况
1 | type F1 = (arg1: string, arg2: string) => string |
数组类型
TypeScript 中有两种定义数组类型的方式,例如定义成员全部为 number
类型的数组
1 | let arr1: Array<number>; |
从定义方式上可以看出,TypeScript 中,数组成员必须全部都为同一类型。若希望成员类型不同,则需要借助联合类型
1 | let arr1: (number | string)[]; |
至于数组的长度,TypeScript 并不会对其进行限制,这就代表着数组成员可以动态变化,TypeScript 并不会对数组的边界进行检测。
这就导致了一个问题,在直接获取数组成员时,可能会得到错误的类型结果
1 | const arr: number[] = []; |
上面 firstEl
的类型被推断为 number
,但很明显,它的结果为 undefined
。
获取成员类型
TypeScript 允许使用类似对象属性访问的方式获取指定成员的类型
1 | let el: Array<bigint>[0]; // el: bigint |
而我们知道,数组的键值都是 number
类型。因此使用下面的方式可以得到同样的效果
1 | let el: Array<bigint>[number]; // el: bigint |
开发中建议使用后者这种方式。
类型推断
在未显示标注类型时,TypeScript 会对数组类型进行推断,此时结果会根据数组的初始值的不同而有所不同。
1 | const arr1 = []; // arr1: any[] |
初始值为空数组的特殊情况
此时虽然初始值被自动推断为 any
,但在后续更新数组内容时,它的类型将会被自动更新。
1 | const arr = []; |
尽管自动更新了类型,但在添加时并不会进行阻拦。比如上面示例中,在 arr.push( "1" )
时 arr
的类型已经变成了 number[]
,但依旧能正常添加字符串元素,并不会报错。
而如果我们在一个崭新的目录里编写上面的案例,反而会得到这个结果:
1 | const arr = []; // arr: never[] |
经过尝试后,得到在两种情况下会出现这种现象:
- 当前项目结构下不存在 tsconfig.json
strict
为true
且同时noImplicitAny
为false
noImplicitAny
明明是一个约束 any
的配置,却在这种特定的组合下起了反作用。
官方在当年的 issue 里将这种现象描述为是这两个配置项的特定组合导致的意外结果,修复难度较大不打算修复。
目前应该也是延续到现在了,注意一下这种情况即可。
只读数组
正常的数组类型是可以随便更改数组成员的,包括成员值与成员数量。
但如果我们确实想定义一个不发生变更的常量数组,可以使用 readonly type[]
只读数组类型
1 | const CONST_ARR: readonly number[] = [1, 2, 3]; |
readonly number[]
与 number[]
并不是同一个类型,前者在 TypeScript 中为后者的子类型。
所以不难理解上面示例的表现行为,即只读数组类型上并不存在 push
属性。
既然是父子类型,那么这俩同样遵守类型兼容中的规则,因此在需要数组类型的环境时,使用只读数组类型会出现错误
还需要注意的一点,只读数组类型不能使用数组的泛型写法,会报错
1 | const CONST_ARR: readonly Array<number> = [1, 2, 3]; // 报错: `readonly` type modifier is only permitted on array and tuple literal types. |
只读数组的另一种定义方式,是使用 const
断言,这回将变量断言为只读元组类型
1 | const CONST_ARR = <const>[ 1, 2 ,3 ]; // CONST_ARR: readonly [1, 2, 3] |
元组
元组是 TypeScript 中特有的类型,JavaScript 中并没有明确定义这种类型,其表示一个成员类型可以自由设置的数组。
所以元组在定义时,必须要明确声明每个成员的类型。
1 | const normal: [number, string, boolean] = [9, "baka", true]; |
元组的元素类型可以添加 ?
修饰符,表示这个类型是可选的。可选类型必须被放置在最后
1 | const noraml: [number, string, boolean?] = [9, "baka"]; |
自定义类型
通过接口与类型别名,可以自定义自己的类型
接口
类型别名
使用 type
关键字来自定义一个类型
1 | type Name = string; |
通常建议类型别名以大驼峰格式命名,方便与变量名区分。
块级作用域
类型别名、接口、枚举均拥有块级作用域,即:在块作用域内定义这些类型时,作用域以外的地方无法使用。
1 | { |
枚举
运算符
typeof
提取一个变量的类型,只能在类型系统中使用,不能参与值运算说多少遍了,TypeScript 不参与运行时
它和 JavaScript 里的 typeof 长得一样但不是一个东西
1 | // 解析值类型 |
字面量类型/单元类型
typescript
允许模拟定义具体值类型
1 | // 限制str的值,使其不允许被修改(模拟const) |
一般情况下, typescript
默认认为对象的属性是可以改变的,但在如下情况会出现问题
1 | function handle(method: "GET" | "POST") {} |
因为默认推断了 obj.method
为 string
类型,然而 string
类型是有可能不属于 'GET' | 'POST'
类型的,因此报错
解决方法如下
1 | // 将obj内属性设置为只读 |
bigint
指非常大的整数,使用时需将 target
设置为 es2020
1 | const bigintNum: bigint = 100n; |
never
never 指为不存在的类型,永远不会被观察到的值,它是所有类型的子类型,用集合来解释即:never 是空集
例如下情况:
1 | type Circle = { |
当一个函数抛出一个异常、中止程序执行 或 死循环,返回值的类型就是 never
,也用于联合类型没有匹配到任何东西的情况下的值类型
void
在 ts 中,一个不返回任何值的函数将自动推断返回值为 void
类型
而在 js 中,这种函数的返回值默认为 undefined
ts 中的 void
和 undefined
是不一样的
object
该类型指的是任何不是基元的值,其并不同于 {}
,也不同于 Object
基元: string number bigint boolean symbol null undefined
Function
注意大写,该类型描述了诸如 bind call apply
以及其他存在于 js 中所有函数值的属性。
Function
类型的值总是可以被调用,调用的方法均返回 any
。因此,并不推荐使用 Function
去定义函数类型。
函数
定义方法
类型别名和接口定义函数的方法分别为
1 | /* 接口 */ |
注:在函数类型的形参中,ts 仅限制了形参的最大数量以及每个形参的类型,对形参的名字无限制,允许定义的形参少于类型中形参的个数
1 | type Handle = (arg1: number, arg2: booleab) => boolean; |
函数签名
函数允许自带属性,类型定义方式如下
1 | type Handle = { |
调用签名
即为函数的类型,如下表示:此函数有一个 string
类型的参数,无返回值的。且存在一个类型为 string
的属性 desc
1 | interface IFunc { |
构造签名
即为 class 的类型
1 | class TsClass { |
若将形参 tsc
的类型修改为 TsClass
,则会出现以下错误
1 | function getTsClass(tsc: TsClass) { |
可选参数
不建议在回调函数中定义可选参数
函数重载
提供 {}
表示对函数的实现,这里称其为为 实现函数,反之为 重载函数
实现函数虽然定义了参数可选,但那是为了兼容两个重载函数,调用时仍然只能使用两种重载的情况
1 | function overload(timeStamp: number): Date; |
调用函数时,是看不到实现函数的参数的,只能看到重载函数的参数
1 | function overload(field: string): void; |
实现函数若存在参数,则必须要兼容所有的重载函数的参数类型,返回值同样需要兼容
1 | function overload(field: string): string; |
可以的话,更推荐用 联合类型的参数
而不是重载参数,因为 重载参数 不能使用两者均有可能的值作为参数。
1 | function overload(field: string): void; |
this 入参
js 中不支持 this 作为函数参数,但 ts 允许这样做。
ts 允许在函数入参列表中第一个位置处,手动写入this
并标记其类型。它仅仅是一个形式上的参数,并不会在编译后的代码中出现,仅仅是为了提供给 Typescript 编译器作为检查静态类型使用。
通常我们会在需要纠正 this 类型的场合下使用这个机制,参考如下示例。
1 | class Handle { |
由于众所周知的 this 指向问题,上面代码中在调用 onClick 时,this 指向 undefined
,而在 onClick 函数中期望的 this 则是指向 Handle
类。经过限制后 ts 捕获到了错误并作出了提醒。若不做限制(两处有任意一处未对 this 进行定义),则会出现 Cannot set property 'value' of undefined
报错。
参数展开
ts 可以给 es6
中的函数形参展开式定义类型
1 | // 此处 m 若不做具体类型定义,则为 any[] 类型 |
同理,也可以展开数组实参传入方法,如下:
1 | const list: string[] = []; |
参数解构
1 | type Parm = { |
函数的 void 返回类型
当把函数类型定义为 返回 void 的函数
时,函数内允许返回任意类型,ts 会将其忽略,执行函数得到的结果被 ts 判断为 void
类型
1 | type VoidFun = () => void; |
但若直接显式的给函数定义返回值类型为 void
,那么这个函数必须不能返回任何值
1 | // 不能将类型“boolean”分配给类型“void”。 |
对象
定义方法
常见有下面几种定义方法
1 | // 匿名 |
属性修饰符
可选属性
1 | interface INormal { |
只读属性
1 | interface INormal { |
索引签名
格式为 { [key: KeyType]: ValueType }
其中 KeyType
仅能使用 number
、string
、symbol
和 模板字符串类型(类型章节有讲)
1 | // 基本使用 |
与 Record
区别,Record 的 key 支持使用 字符串字面量类型与联合类型
类型扩展
接口
使用 extends
关键字对接口进行扩展,在后面继承部分会详细说明
接口和类型别名可以互相继承
1 | // 继承单个 |
类型别名
使用交叉类型对类型别名进行扩展,交叉类型会在后面详细说明
接口和类型别名可以互相交叉
1 | type c = a & { |
向现有类型添加字段
类型别名一经创建无法更改,无法添加字段
1 | /* 接口 */ |
类型推断
三元表达式
ts 会根据三元表达式冒号两侧自动推断类型
1 | // string: number | string |
函数返回值
变量经过函数内处理后返回,会自动进行类型缩小
1 | function handle() { |
类型断言
两种断言方法
1 | let str = "string" as any; |
typescript
只允许类型断言转换为更具体或不太具体的类型,如不允许将 number
类型断言为 string
当不知道类型时,断言为 unknown
语义上比 any
更确切
类型谓词 is
对函数返回值定义类型 fieldName is Type
,当函数返回 true
时,变量会被指定为对应的类型
若不使用 is
,函数会被认为返回值类型是简单的 boolean
,变量依然是联合类型,无法确切定义具体类型
1 | type bird = { |
也可以用来过滤数组
1 | const zoos: Array<fish | bird> = [getPet(), getPet(), getPet()]; |
关键字
keyof
提取一个类型的所有键值组成一个新的类型
1 | type Normal = { |
示例:获取一个类型的所有值的类型
1 | type Normal = { |
示例:为获取对象属性的方法定义类型
1 | function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[keyof T] { |
当 keyof
用于基本类型时,将会提取该类型的所有可用方法
1 | // type Num = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString" |
keyof
也可以用于 class,此时不会获取 class 中的 private
和 protected
属性
1 | class Base { |
keyof
可以获取枚举类型,但直接使用会获得枚举属性的方法,想获取她的键值,需要结合 typeof
使用
1 | enum HttpMethod { |
对索引签名使用
1 | interface normal { |
对类使用
1 | class Normal { |
typeof
提取一个变量的类型
1 | // 解析函数 |
提取 any 和 unknown 类型
对 any
和 unknown
进行类型提取时则有比较特殊的结果
1 | // type Any = string | number | symbol |
infer
infer
关键字用于声明类型变量,以存储在模式匹配过程中所捕获的类型
下面这个示例就捕获到了 number[]
中的 number
类型
1 | type Normal<T> = T extends (infer U)[] ? U : T; |
声明两个类型变量,捕获对象中的类型
1 | type Normal<T> = T extends { label: infer U; value: infer R } ? [U, R] : T; |
infer 使用注意:
- 仅可以在条件类型的
extends
子句中使用 infer
声明的类型变量 仅可以在条件类型的true
分支使用
协变与逆变
改编上面示例,两个 infer 均指向同一个变量,结果为联合类型 (协变位置)
1 | type Normal<T> = T extends { label: infer U; value: infer U } ? U : T; |
再做改编,将 infer 指向函数形参,结果为交叉类型 (逆变位置)
1 | type Base = { |
函数参数是逆变的,而对象属性是协变的。
在逆变位置的同一类型变量中的多个候选会被推断成交叉类型。
类
基本结构
1 | class Normal { |
构造函数
构造函数在执行 new 创建对象时执行,内部允许初始化类的成员属性
1 | class Normal { |
有两点要注意:
- 构造函数不能有类型参数
- 构造函数不能有返回类型注释
允许对构造函数使用参数重载
1 | class Normal { |
参数属性
即在构造函数 constructor
参数上定义类成员,由 readonly、public、protected、private
等修饰
1 | class Normal { |
Getters/Setters
为两组方法,格式如下:
1 | class Normal { |
存在以下几点特性:
- 如果存在 getter 但不存在 setter,该属性自动变为只读属性
- 如果没有指定 setter 参数的类型,会自动从 getter 的返回值类型推断出来
- getter 和 setter 必须设置相同的成员可见性
setter 的参数类型
Typescript 4.3 后,允许 setter 的参数类型为 getter 返回值的子类型
1 | class Normal { |
索引签名
一旦限制索引签名,类中的属性和方法都将受到约束
1 | class Normal { |
implements
实现一个接口或类型别名,当一个类指定 implements
后,必须实现其中的所有必须属性。格式如下:
1 | interface Normal { |
一个类可以实现多个 implements
1 | interface Normal { |
implement
仅对class
的成员进行限制,并不会给改变类的类型
1 | interface Normal { |
extends
(派生类)继承另一个类(基类),获取其所有非 private
修饰的属性。
当一个类继承另一个类时,若该类存在 constructor
构造函数,则必须指定 super
调用。
若派生类未显示写明构造函数,则默认使用 基类 的构造函数。
super: 调用父类的构造函数
1 | class Base { |
派生类为基类的子类型,允许被赋值
1 | const base: Base = new Normal(); |
重写基类方法
派生类允许使用和基类同名的方法,来对其进行重写
但必须要与基类方法兼容,如参数以及返回值类型,类似函数重载(属性也是如此)
1 | class Base { |
初始化顺序
当派生类使用 new
关键字创建对象时,内部按照以下顺序执行
- 字段初始化 (基类)
- 构造函数运行 (基类)
- 字段初始化 (派生类)
- 构造函数运行 (派生类)
es5 继承内置对象问题
当 typescript.json
中的 target
设置为 es5
时,使用 extends
继承内置对象时会出现一些问题
1 | class BakaError extends Error { |
编译未报错,运行如下代码,发现运行报错
1 | // TypeError: bakaError.echo is not a function |
原因:由于 es5 并没有 class 这种玩意儿,需要手动设置一下原型对象
对 constructor 进行如下修改
1 | class BakaError extends Error { |
再次执行,正常运行
1 | // 代码出错了笨蛋!: abaaba |
类中的 this
由于 js 遗留问题,类方法内的 this 可能会存在乱指向的问题, 比如下面示例
1 | class Normal { |
改编为 箭头函数 即可解决这种情况
1 | class Normal { |
注:改编为箭头函数后,派生类无法再使用 super
调用该方法。 typescript
并不会报错,但在运行时将会抛出错误
1 | class Base { |
修饰符
readonly
被修饰的属性不允许在构造函数以外的地方进行修改
1 | class Normal { |
public
表示类成员在任何地方都可以访问,是默认值,不指定时默认为 public
。
protected
protected
修饰的成员只对当前类和子类可见。
1 | class Base { |
派生类可以暴露基类中 protocted
所修饰的成员。
1 | class Base { |
private
private
修饰的成员只对当前类可见。
static
static
修饰的成员为静态成员,访问其不需要实例化,直接用类名就可以访问
1 | class Base { |
静态成员允许被继承,且可以被派生类直接用类名访问
1 | class Base { |
es2015 以上支持使用 专用标识符 #
修饰静态成员,如下例的 #label
。
修饰后的成员变为私有属性,仅允许类内部通过类名调用。
且 static
支持修饰一个代码块,称为 static 区块,区块内同样可以访问 #
静态成员
1 | class Normal { |
在泛型类中,static
修饰的成员是不能使用 类型参数 的
1 | class Normal<T> { |
类表达式
类允许像方法一样用表达式声明一个匿名类并赋值,同样允许使用泛型
1 | const Normal = class<T> { |
this 类型
在类中可以使用 this
类型来指向当前 this
所指向的对象
1 | class Base { |
this is Type
在类和接口的方法的返回值的位置使用,缩小调用该方法的目标的类型
1 | class Normal<T> { |
抽象类与抽象成员
使用 abstract
修饰后即变为抽象类,抽象类不能被实例化,只能被作为基类继承。
使用 abstract
修饰的成员为抽象成员,abstract
关键字于 public
readonly
等之后使用,仅允许出现在抽象类中,不可以实现,子类继承后必须实现该成员。
1 | abstract class Base { |
类之间的细节补充
当两个类的成员和类型完全相同时,是可以互相替代的
1 | class Normal { |
当两个类的成员存在兼容关系但未显式继承时,也是存在隐式继承的
1 | class Normal { |
针对空类时,任何类型都可以是它的实例对象
1 | class Empty {} |
泛型
定义泛型
1 | // 泛型对象 |
自动推断
泛型是具备自动推断的,下面的示例自动将 T 推断为了 number
1 | function genericsFunc<T>(tList: T[]): T | undefined { |
定义多个泛型别名
1 | function genericsFunc<I, O>(arr: I[], func: (arg: I[]) => O): O[] { |
指定类型参数
有时候通过自动推断并不能实现预期效果,这时候就要手动添加类型
1 | function combine<T>(arr1: T[], arr2: T[]): T[] { |
泛型类
1 | class Normal<T> { |
泛型约束
有时需要使泛型变量受到某些约束,例如必须存在 length
属性,就需要对泛型进行限制
1 | function checkLonger<T extends { length: number }>(a: T, b: T): T { |
在泛型约束中使用类型参数
1 | interface INormalArg { |
对 class 类型进行泛型约束
1 | class Normal { |
编写通用函数准则
- 可以的话,使用类型本身,而不是对其约束
- 尽可能少的使用指定类型参数
- 如果一个类型参数只出现在一个地方,则需要考虑是否真的需要它
其他类型
索引访问类型
1 | type Normal = { |
结合 keyof
使用,获取接口或类型别名的所有类型
1 | type Normal = { |
结合 typeof
使用,通过特殊类型索引 number
, 获取数组子项类型
*原因:数组的默认属性类型即为 number
1 | const normalArray = [ |
映射类型
1 | type Mapped<K> = { [ P in K ]: T } |
P in K
类似 js 中的 for in
语句,用于遍历 K 中的所有类型,T
为你希望该属性所表示的任意类型
添加修饰符
允许使用修饰符,在修饰符前面添加 +
或 -
表示 添加/移除 修饰符,默认为 +
使用 readonly
和 ?
修饰符:
1 | // type Normal = { readonly x?: number | undefined; readonly y?: number | undefined } |
as 重新映射
ts 4.1
开始支持,允许对键值重新映射,格式如下
1 | type KeyRemapping<T> = [ K in keyof T as NewKeyType ]: T[K] |
NewKeyType
必须为 string | number | symbol
的子类型
在对键进行重新映射时,若 as
子句返回 never
类型,该键将被删除
示例:将普通接口类型映射为 getter
格式的类型,即 getXxx: () => Type
:
1 | type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }; |
其中:
- `` 为模板字面量类型,后面将会说明
- Capitalize:将
string
类型转换为首字母大写 - string & K:利用交叉类型返回
never
,过滤非string
类型
条件类型
类似 js 中的 三元表达式
格式
1 | type Type = T extends U ? X : Y; |
结合泛型使用
1 | type Normal = { |
结合 infer 关键字实现类型分发
下面这个案例封装了一个获取函数类型返回值的类型
1 | type GetReturnType<T extends (...args: any[]) => any> = T extends ( |
其实这就是预定义类型 ReturnType
的实现方法
分布式条件类型
如果被检查的类型,即 T extends U ? X : Y
中的 T
是一个裸类型参数,则该条件类型为分布式条件类型
裸类型参数:没有被数组(T[]
)、元组([T]
)或Promise(Promise<T>
)包装过
对于分布式条件类型,若传入的被检查的参数为联合类型时,在运算过程中将被分解为多个分支
1 | type Normal = A | B | C extends U ? X : Y |
下面展示了是否是分布式条件类型时,传入的参数得到的最终结果的不同
1 | // 分布式条件类型 |
模板字面量类型
ts 4.1 开始支持,写法类似 js 中的模板字符串,格式如下
其中类型占位符 T
允许为 string | number | boolean | bigint
的子类型,也可以传入对应类型的类型别名。
1 | type Model<T> = `${T}` |
使用示例:
1 | type Base<T extends string> = `chiruno is ${ T }` |
当类型占位符 T 为联合类型时,联合类型会被自动展开
而对于包含多个类型占位符的情况时,多个联合类型将被展开并解析为叉积
1 | // 联合类型自动展开 |
模板字面量类型可以将非字符串类型的基本类型字面量转换为对应的字符串类型字面量
1 | type ToString<T extends number | string | boolean | bigint> = `${T}` |
结合 infer
和 条件类型 进行推断
1 | type Direction = "left" | "top" | "right" | "bottom"; |
预定义类型
ReturnType
使用方式:ReturnType<T>
,T 为一个函数类型
获得一个函数类型的返回值类型
1 | type Normal = (arg: string) => boolean; |
Uppercase & Lowercase & Capitalize & Uncapitalize
分别为 全部大写、全部小写、首字母大写、首字母小写, 仅对 string
类型进行处理的内置类型
1 | // type Normal = "BAKA" |
声明文件
npm 包的声明文件
与 npm 包绑定的声明文件
在两种情况下,声明文件会和 npm 包绑定
- 在包目录下存在类型文件
index.d.ts
package.json
中显式的定义了types
属性,指向具体位置的声明文件,如"types": "index.d.ts"
第三方声明文件
有些时候,npm 包维护者并没有提供声明文件,而由社区中其他人向 @types
贡献了声明文件,可以尝试安装一下对应的 @types
包
1 | npm i @types/[package name] --save-dev |
默认情况下,typescript 会自动包含支持全局使用的任何声明定义,如安装 @types/node
后可以在全局使用 process
。
但如果你不希望被各种全局变量污染作用域,可以在 tsconfig.json
中作如下配置。
1 | { |
像这样配置了 types 以后,将只允许使用 node 的 @types
包,即使安装了其他的 @types
包,也不会生效,除非同样将其加入到这里。
自行定义声明文件
若该 npm 包连第三方声明文件都不存在,这时候就只能自行定义声明文件了
- 创建一个
types
目录,将marry
的声明文件放到其中,目录为types/marry/index.d.ts
。并配置tsconfig.json
中的baseUrl
和path
字段,如下。
1 | { |