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;
 
 
  |