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;
|
接口
接口(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';
|
描述函数:
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);
|
枚举的值默认从0开始,也可以手动指定:
1 2 3 4
| enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; console.log(c);
|
枚举的值可以是字符串:
1 2 3 4
| enum Color {Red = 'red', Green = 'green', Blue = 'blue'}
let c: Color = Color.Green; console.log(c);
|
枚举的值可以是计算出来的:
1 2 3 4
| enum Color {Red = 'red'.length, Green = 'green'.length, Blue = 'blue'.length}
let c: Color = Color.Green; console.log(c);
|
字面量类型
TypeScript 还支持字面量类型
,也就是类似 1111、’aaaa’、{ a: 1} 这种值也可以做为类型。
字符串的字面量类型有两种,一种是普通的字符串字面量,比如 ‘aaa’,另一种是模版字面量,比如 aaa${string},它的意思是以 aaa
开头,后面是任意 string 的字符串字面量类型。
1 2 3 4 5 6 7
| let a: 'aaa'; a = 'aaa'; a = 'bbb';
let b: `aaa${string}`; b = 'aaa'; b = 'aaabbb';
|
还有四种特殊的类型:void
、never
、any
、unknown
:
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 类型。
交叉:&
交叉类型(Intersection Type)是指一个变量可以同时拥有多种类型的属性,比如 number & string,表示这个变量既拥有 number
类型的属性,也拥有 string 类型的属性。
1
| type res = { a: 1 } & { b: 2 };
|
注意,同一类型可以合并,不同的类型没法合并,会被舍弃:
1
| type res = { a: 1 } & { a: 2 };
|
映射类型
对象、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
|
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
| 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; }
|
指定索引和值的类型分别为 K
和 T
,就可以创建一个对应的索引类型。
上面的索引类型的约束我们用的 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
,否则索引设置为 never
,never
的索引会在生成新的索引类型时被去掉。
值保持不变,依然为原来索引的值,也就是 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 ReverseArr<Arr extends Array<unknown>> = Arr extends [infer First, ...infer Rest] ? [...ReverseArr<Rest>, First] : Arr;
|
Includes
1 2 3
|
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
| 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
|
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<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;
|