TypeScript通关秘籍

TS类型系统支持哪些类型和类型运算

TS中的类型

静态类型系统的目的是把类型检查从运行时提前到编译时。

1
2
3
4
基础类型:number、boolean、string、object、symbol、undefined、null
包装类型:Number、Boolean、String、Object、Symbol
复合类型:class、Array
新增了三种类型:Tuple(元组)、Enum(枚举)、Interface(接口)

元组

元组是一种特殊的数组,它限定了数组元素的类型和个数。

1
2
3
4
let tuple: [string, number] = ['a', 1];
tuple[0] = 'b';
tuple[1] = 2;
tuple[2] = 3; // Error: 不能将类型“3”分配给类型“undefined”

接口

接口(Interface)是一种类型,它可以描述函数、对象、构造器的结构:

描述对象:

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age: number;
}

let person: Person = {
name: 'Tom',
age: 25
};
person.name = 'Jerry';
person.age = '25'; // Error: 不能将类型“"25"”分配给类型“number”

描述函数:

1
2
3
4
5
6
7
8
9
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
return source.search(subString) !== -1;
};
mySearch('abc', 'a');

描述构造器:

1
2
3
4
5
6
7
interface PersonConstructor {
new(name: string, age: number): IPerson;
}

function createPerson(ctor: PersonConstructor): IPerson {
return new ctor('guang', 18);
}

对象类型、class类型在TS中也叫做索引类型,因为它们都可以通过索引的方式访问属性。对象可以动态添加属性,如果不知道有什么属性,可以用索引签名。

1
2
3
4
5
6
7
8
9
10
interface IPerson {
name: string;
age: number;

[propName: string]: string | number;
}

const obj: IPerson = {}
obj.name = 'hualu'
obj.age = 18

总之,接口可以用来描述函数、构造器、索引类型(对象、class、数组)等复合类型。

枚举

枚举(Enum)是一系列值的复合:

1
2
3
4
enum Color {Red, Green, Blue}

let c: Color = Color.Green;
console.log(c); // 1

枚举的值默认从0开始,也可以手动指定:

1
2
3
4
enum Color {Red = 1, Green = 2, Blue = 4}

let c: Color = Color.Green;
console.log(c); // 2

枚举的值可以是字符串:

1
2
3
4
enum Color {Red = 'red', Green = 'green', Blue = 'blue'}

let c: Color = Color.Green;
console.log(c); // green

枚举的值可以是计算出来的:

1
2
3
4
enum Color {Red = 'red'.length, Green = 'green'.length, Blue = 'blue'.length}

let c: Color = Color.Green;
console.log(c); // 5

字面量类型

TypeScript 还支持字面量类型,也就是类似 1111、’aaaa’、{ a: 1} 这种值也可以做为类型。

字符串的字面量类型有两种,一种是普通的字符串字面量,比如 ‘aaa’,另一种是模版字面量,比如 aaa${string},它的意思是以 aaa
开头,后面是任意 string 的字符串字面量类型。

1
2
3
4
5
6
7
let a: 'aaa';
a = 'aaa';
a = 'bbb'; // Error: 不能将类型“"bbb"”分配给类型“"aaa"”

let b: `aaa${string}`;
b = 'aaa';
b = 'aaabbb';

还有四种特殊的类型:voidneveranyunknown

never 代表不可达,比如函数抛异常的时候,返回值就是 never。

void 代表空,可以是 undefined 或 never。

any 是任意类型,任何类型都可以赋值给它,它也可以赋值给任何类型(除了 never)。

unknown 是未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型。

这些就是 TypeScript 类型系统中的全部类型了,大部分是从 JS 中迁移过来的,比如基础类型、Array、class 等,也添加了一些类型,比如
枚举(enum)、接口(interface)、元组等,还支持了字面量类型和 void、never、any、unknown 的特殊类型。

类型的装饰

除了描述类型的结构外,TypeScript 的类型系统还支持描述类型的属性,比如是否可选,是否只读等:

1
2
3
4
5
6
interface IPerson {
readonly name: string;
age?: number;
}

type tuple = [string, number?];

TypeScript 类型系统中的类型运算

条件:extends ? :

TypeScript 里的条件判断是 extends ? :,叫做条件类型(Conditional Type)。

1
type res = 1 extends 2 ? true : false;

这就是 TypeScript 类型系统里的 if else。

但是,上面这样的逻辑没啥意义,静态的值自己就能算出结果来,为什么要用代码去判断呢?

所以,类型运算逻辑都是用来做一些动态的类型的运算的,也就是对类型参数的运算。

1
2
3
type isTwo<T> = T extends 2 ? true : false;
type res = isTwo<1>;
type res2 = isTwo<2>;

这种类型也叫做高级类型

高级类型的特点是传入类型参数,经过一系列类型运算逻辑后,返回新的类型。

推倒:infer

如何提取类型的一部分呢?答案是 infer。

比如提取元组类型的第一个元素:

1
2
3
type First<Tuple extends unknown[]> = Tuple extends [infer T, ...infer R] ? T : never;

type res = First<[1, 2, 3]>;

联合:|

联合类型(Union Type)是指一个变量可以有多种类型,比如 number | string,表示这个变量可以是 number 类型,也可以是 string 类型。

1
type res = 1 | 2 | 3;

交叉:&

交叉类型(Intersection Type)是指一个变量可以同时拥有多种类型的属性,比如 number & string,表示这个变量既拥有 number
类型的属性,也拥有 string 类型的属性。

1
type res = { a: 1 } & { b: 2 };

注意,同一类型可以合并,不同的类型没法合并,会被舍弃:

1
type res = { a: 1 } & { a: 2 };  // res:never

映射类型

对象、class 在 TypeScript 对应的类型是索引类型(Index Type),那么如何对索引类型作修改呢?

答案是映射类型。

1
2
3
type MapType<T> = {
[Key in keyof T]?: T[Key]
}

keyof T是查询索引类型中所有的索引,叫做索引查询

T[Key]是查询索引类型中索引对应的类型,叫做索引访问

in是用于便利联合类型的运算符。

比如我们把一个索引类型的值变成 3 个元素的数组:

1
2
3
4
5
type MapType<T> = {
[Key in keyof T]: [T[Key], T[Key], T[Key]]
}

type res = MapType<{ a: 1, b: 2 }>;

除了值可以变化,索引也可以做变化,用 as 运算符,叫做重映射

比如我们把一个索引类型的值变成 3 个元素的数组:

1
2
3
4
5
type MapType<T> = {
[Key in keyof T as `new_${Key}`]: T[Key]
}

type res = MapType<{ a: 1, b: 2 }>;

套路一:模式匹配做提取

模式匹配

比如这样一个 Promise 类型:

1
type p = Promise<'guang'>

我们想提取value的类型,可以这样做:

1
type res = p extends Promise<infer T> ? T : never;

通过 extends 对传入的类型参数 P 做模式匹配,其中值的类型是需要提取的,通过 infer 声明一个局部变量 Value 来保存,如果匹配,就返回匹配到的
Value,否则就返回 never 代表没匹配到。

这就是 Typescript 类型的模式匹配:

Typescript 类型的模式匹配是通过 extends 对类型参数做匹配,结果保存到通过 infer 声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。

数组类型

数组类型想提取第一个元素的类型怎么做呢?

1
type GetArrFirst<Arr extends unknown[]> = Arr extends [infer T, ...infer arr] ? T : never;

构造器

构造器函数的区别是,构造器是用于创建对象的,所以可以被new

套路二:重新构造做变换

1
2
3
4
type Zip<One extends [unknown, unknown], Other extends [unknown, unknown]> =
One extends [infer OneFirst, infer OneSecond] ? (Other extends [infer OtherFirst, infer OtherSecond] ? [[OneFirst, OtherFirst], [OneSecond, OtherSecond]] : []) : [];

type ZipResult = Zip<[1, 2], ['huang', 'jie']>
1
2
3
4
// 首字母大写
type CapitalizeStr<Str extends string> = Str extends `${infer first}${infer left}` ? `${Uppercase<first>}${left}` : Str

type UseString = CapitalizeStr<'huang'>
1
2
3
4
5
// 实现 dong_dong_dong 到 dongDongDong 的变换

type CamelCase<Str extends string> = Str extends `${infer left}_${infer right}${infer rest}` ? `${left}${Uppercase<right>}${CamelCase<rest>}` : Str

type tryCamelCase = CamelCase<'dong_dong_dong'>
1
2
3
4
5
// 删除字符串中的某个子串

type DropSubStr<Str extends string, SubStr extends string> = Str extends `${infer prefix}${SubStr}${infer suffix}` ? DropSubStr<`${prefix}${suffix}`, SubStr> : Str

type TeyDropSubStr = DropSubStr<'hello,world', 'o'>
1
2
3
4
5
6
7
8
9

// 函数类型的重新构造
type AppendArgument<Func extends Function, Arg> = Func extends (...args: infer Args) => infer ReturnType ? (...args: [...Args, Arg]) => ReturnType : never

function Add(a: number, b: number): number {
return a + b
}

type TryAppendArgument = AppendArgument<typeof Add, 3>

索引类型的重新构造

索引类型是聚合多个元素的类型,class、对象等都是索引类型,比如这就是一个索引类型:

1
2
3
4
5
type obj = {
name: string;
age: number;
gender: boolean;
}

索引类型可以添加修饰符 readonly(只读)、?(可选):

1
2
3
4
5
type obj = {
readonly name: string;
age?: number;
gender: boolean;
}

Mapping

对它的修改和构造新类型涉及到了映射类型的语法:

1
2
3
type Mapping<Obj extends object> = {
[Key in keyof Obj]: Obj[Key]
}

keyof 取出 Obj 的索引,作为新的索引类型的索引,也就是 Key in keyof Obj

UppercaseKey

除了可以对 Value 做修改,也可以对 Key 做修改,使用 as,这叫做重映射

1
2
3
4
// 把索引类型的key变成大写
type UppercaseKey<Obj extends object> = {
[Key in keyof Obj as Uppercase<Key & string>]: Obj[Key]
}

Record

TypeScript 提供了内置的高级类型 Record 来创建索引类型:

1
type Record<K extends string | number | symbol, T> = { [P in K]: T; }

指定索引和值的类型分别为 KT,就可以创建一个对应的索引类型。

上面的索引类型的约束我们用的 object,其实更语义化一点我推荐用 Record<string, any>:

1
2
3
type UppercaseKey<Obj extends Record<string, any>> = {
[Key in keyof Obj as Uppercase<Key & string>]: Obj[Key]
}

去掉readonly修饰

1
2
3
type RemoveReadonly<Obj extends Record<string, any>> = {
-readonly [Key in keyof Obj]: Obj[Key]
}

去掉可选修饰符

1
2
3
type RemoveOptional<Obj extends Record<string, any>> = {
[Key in keyof Obj]-?: Obj[Key]
}

FilterByValueType

可以在构造新索引类型的时候根据值的类型做下过滤:

1
2
3
4
5
6
7
type FilterByValueType<
Obj extends Record<string, any>,
ValueType
> = {
[Key in keyof Obj as Obj[Key] extends ValueType ? Key : never]
: Obj[Key]
}

如果原来索引的值 Obj[Key]ValueType 类型,索引依然为之前的索引 Key,否则索引设置为 nevernever
的索引会在生成新的索引类型时被去掉。

值保持不变,依然为原来索引的值,也就是 Obj[Key]

这样就达到了过滤索引类型的索引,产生新的索引类型的目的。


递归复用做变换

递归是把问题分解为一系列相似的小问题,通过函数不断调用自身来解决这一个个小问题,直到满足结束条件,就完成了问题的求解。

TypeScript
类型系统不支持循环,但支持递归。当处理数量(个数、长度、层数)不固定的类型的时候,可以只处理一个类型,然后递归的调用自身处理下一个类型,直到结束条件也就是所有的类型都处理完了,就完成了不确定数量的类型编程,达到循环的效果。

Promise 的递归复用

DeepPromiseValueType

先用 Promise 热热身,实现一个提取不确定层数的 Promise 中的 value 类型的高级类型。

1
2
3
4
5
6
    type ttt = Promise<Promise<Promise<Record<string, any>>>>;
type DeepPromiseValueType<P extends Promise<unknown>> =
P extends Promise<infer ValueType>
? ValueType extends Promise<unknown>
? DeepPromiseValueType<ValueType> : ValueType
: never;

其实这个类型的实现可以进一步的简化:

1
2
3
type DeepPromiseValueType<P> =
P extends Promise<infer ValueType>
? DeepPromiseValueType<ValueType> : P;

数组类型的递归

ReverseArr

1
2
3
4
// 将 type arr = [1,2,3,4,5] 变成 type arr = [5,4,3,2,1]
// 假如数组长度不确定,怎么做呢? 用递归!

type ReverseArr<Arr extends Array<unknown>> = Arr extends [infer First, ...infer Rest] ? [...ReverseArr<Rest>, First] : Arr;

Includes

1
2
3
// 查找 [1, 2, 3, 4, 5] 中是否存在 4,是就返回 true,否则返回 false。

type Includes<Arr extends Array<unknown>, FindItem> = Arr extends [infer First, ...infer Rest] ? (First extends FindItem ? true : Includes<Rest, FindItem>) : false;

RemoveItem

可以查找自然就可以删除,只需要改下返回结果,构造一个新的数组返回。

1
2
3
4
5
6
type RemoveItem<Arr extends unknown[], Item, Result extends unknown[] = []>
= Arr extends [infer First, ...infer Rest]
? (First extends Item
? RemoveItem<Rest, Item, Result>
: RemoveItem<Rest, Item, [...Result, First]>)
: Result;

BuildArray

1
2
3
4
// 传入 5 和元素类型,构造一个长度为 5 的该元素类型构成的数组。
type BuildArray<Length extends number, Ele = unknown, Arr extends unknown[] = []> = Arr['length'] extends Length ? Arr : BuildArray<Length, Ele, [Ele, ...Arr]>

type BuildArrResult = BuildArray<5, string>

字符串类型的递归

在类型体操里,遇到数量不确定的问题,就要条件反射的想到递归。

ReplaceAll

1
2
3
4
5
6
7
8
9
type ReplaceAll<
Str extends string,
From extends string,
To extends string
> = Str extends `${infer Left}${From}${infer Right}`
? `${Left}${To}${ReplaceAll<Right, From, To>}`
: Str;

type ReplaceAllResult = ReplaceAll<'hello,world', 'o', 'a'>

StringToUnion

把字符串字面量类型的每个字符都提取出来组成联合类型,也就是把 ‘dong’ 转为 ‘d’ | ‘o’ | ‘n’ | ‘g’。

1
2
3
4
5

// 首字母放到first字段,其余字符放到Rest字段
type StringToUnion<Str extends string> = Str extends `${infer first}${infer Rest}` ? first | StringToUnion<Rest> : never

type StringToUnionResult = StringToUnion<'dong'>

ReverseStr

1
2
3
type ReverseStr<Str extends string, Result extends string = ''> = Str extends `${infer First}${infer Rest}` ? ReverseStr<Rest, `${First}${Result}`> : Result

type ReverseStrResult = ReverseStr<'dong'>

对象类型的递归

DeepReadonly

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

// 如果对象的层数不确定,就需要递归来解决

// type DeepReadonly<Obj extends Record<string, any>> = {
// readonly [Key in keyof Obj]:
// Obj[Key] extends object
// ? Obj[Key] extends Function
// ? Obj[Key]
// : DeepReadonly<Obj[Key]>
// : Obj[Key]
// }

type DeepReadonly<T extends Record<string, any> = {}> = T extends any ? {
readonly [k in keyof T]: T[k] extends Record<string, any> ? DeepReadonly<T[k]> : T[k]
} : never;

type TestDeepReadonly = DeepReadonly<{
a: {
b: {
c: {
f: () => 'dong',
d: {
e: {
guang: string
}
}
}
}
},
k: 1
}>

元组长度做计算

**TypeScript 类型系统中没有加减乘除运算符,但是可以通过构造不同的元组然后取 length 的方式来完成数值计算,把数值的加减乘除转化为对元组的提取和构造。
**

元组长度实现加减乘除

Add加法

构造两个数组,然后合并成一个,取 length。

1
2
3
4
5
6
7
type BuildArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = []
> = Arr['length'] extends Length
? Arr
: BuildArray<Length, Ele, [...Arr, Ele]>;

类型参数 Length 是要构造的数组的长度。类型参数 Ele 是数组元素,默认为 unknown。类型参数 Arr 为构造出的数组,默认是 []。

基于它就能实现加法:

1
2
type Add<Num1 extends number, Num2 extends number> =
[...BuildArray<Num1>, ...BuildArray<Num2>]['length'];

Subtract减法

1
2
3
4
type Subtract<Num1 extends number, Num2 extends number> =
BuildArray<Num1> extends [...arr1: BuildArray<Num2>, ...arr2: infer Rest]
? Rest['length']
: never;

类型参数 Num1、Num2 分别是被减数和减数,通过 extends 约束为 number。

构造 Num1 长度的数组,通过模式匹配提取出 Num2 长度个元素,剩下的放到 infer 声明的局部变量 Rest 里。

取 Rest 的长度返回,就是减法的结果。

1
type SubstractResult = Subtract<5, 3>

Multiply乘法

1
2
3
4
type Multiply<Num1 extends number, Num2 extends number> =
BuildArray<Num2> extends [...arr: BuildArray<Num1>]
? arr['length']
: never;

TS内置的高级类型有哪些?

Parameters

Parameters用于提取函数类型的参数类型。

1
2
3
4
type Parameters<T extends (...args: any) => any>
= T extends (...args: infer P) => any
? P
: never;

类型参数 T 为待处理的类型,通过 extends 约束为函数,参数和返回值任意。

通过 extends 匹配一个模式类型,提取参数的类型到 infer 声明的局部变量 P 中返回。

ReturnType

ReturnType 用于提取函数类型的返回值类型。

源码是这样的:

1
2
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer P ? P : any

类型参数 T 为待处理的类型,通过 extends 约束为函数类型,参数和返回值任意。

用 T 匹配一个模式类型,提取返回值的类型到 infer 声明的局部变量 P 里返回。

ConstructorParameters

构造器类型和函数类型的区别就是可以被new

Parameters 用于提取函数参数的类型,而 ConstructorParameters 用于提取构造器参数的类型。

1
2
3
4
5
type ConstructorParameters<
T extends abstract new (...args: any) => any
> = T extends abstract new (...args: infer P) => any
? P
: never;

类型参数 T 是待处理的类型,通过 extends 约束为构造器类型,加个 abstract 代表不能直接被实例化(其实不加也行)。

用 T 匹配一个模式类型,提取参数的部分到 infer 声明的局部变量 P 里,返回 P。

InstanceType

提取构造器返回值的类型,就是 InstanceType

1
2
3
4
5
6
type InstanceType<
T extends abstract new (...args: any) => any
> = T extends abstract new (...args: any) => infer R
? R
: any;


TypeScript通关秘籍
https://zouhualu.github.io/20230816/TypeScript通关秘籍/
作者
花鹿
发布于
2023年8月16日
许可协议