TypeScript类型体操汇总

225 阅读8分钟

TypeScript有着各种非常复杂的类型计算逻辑,作为一个初学者,对这一块很是头痛,这里对此做一个总结。

核心知识点
keyof 和 in

在ts中,keyof T 表示获取T类型中所有的属性键

type Person = {
  name: string;
  age: number;
}
// 结果:'name' | 'age'
type result = keyof Person

in右侧通常是一个联合类型,可以使用in来迭代这个联合类型

// 仅演示使用, K为每次迭代的项
K in 'name' | 'age' | 'sex'
K = 'name' // 第一次迭代结果
K = 'age'  // 第二次迭代结果
K = 'sex'  // 第三次迭代结果

比如可以写一些辅助工具,比如Readonly

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
type Person = {
  name: string;
  age: number;
};
// 结果:{ readony name: string; readonly age: number; }
type result = Readonly<Person>;

这里的[P in keyof T]就是遍历T中的每一个属性键,并且赋值为P,相当于for in

typeof

TS中的typeof,可以用来获取一个JavaScript变量的类型,经常用于获取一个普通对象或者一个函数的类型, 比如

const obj = {
  name: 'AAA',
  age: 23
}
type t2 = typeof obj
extends

extends关键词,一般有两种用法:类型约束条件类型

类型约束经常和泛型一起使用,

比如U extends keyof T,就表明U是T属性键中的一种

条件类型和常见的三元表达式一样,

type isTwo<T> = T extends 2 ? true: false;
infer

infer关键词的作用是延时推导,它会在类型未推导时进行占位,等到真正推导成功后,它能准确的返回正确的类型。

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

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

还有这个,提取函数返回类型的工具

type ReturnType<T> = T extends (...args: any) => infer R ? R : neverconst add = (a: number, b: number): number => {
  return a + b
}
// 结果: number
type result = ReturnType<typeof add>
  • T extends (...args: any) => infer R:如果不看infer R,这段代码实际表示:T是不是一个函数类型。
  • (...args: any) => infer R:这段代码实际表示一个函数类型,其中把它的参数使用args来表示,把它的返回类型用R来进行占位。 如果T满足是一个函数类型,那么我们返回其函数的返回类型,也就是R;如果不是一个函数类型,就返回never
模式匹配
数组类型

数组类型想要获取第一个元素,可以这样写:

type GetFirst<arr extends unknown[]> = arr extends [infer F ,...unknown[]] ? F :never;

测试一下:

type GetFirstResult = GetFirst<['123',2,3]>  //'123'

这里用的unknown它和any的都是可以表示任意类型,而any还可以赋值给任意类型。

如果想要获取最后一个类型参数的话,也比较简单:

type GetLast<arr extends unknown[]> = arr extends [...unknown[],infer F] ? F :never;
​
type GetLastResult = GetLast<['123',2,{}]>

如果想要获取最后一个参数之外的剩余数组元素,可以这样写:

type PopArr<Arr> = 
    Arr extends [] ? [] 
        : Arr extends [...infer Rest,unknown] ? Rest : never;
​
type getPopArr = PopArr<['123321',{},1,2,3,4,'123']> //'123321',{},1,2,3,4,

这里的unknown就相当于占位置,通过这个可以获取到除去元素的剩余元素等。

字符串类型

也可以通过匹配模式来判断字符串是否以某个前缀开头:

type StartsWith<Str extends string, Prefix extends string> = 
    Str extends `${Prefix}${string}` ? true : false;
​
type StartsWithResult = StartsWith<'aabbcc','aa'> //true

做字符串替换:

type ReplaceStr<
    Str extends string,
    From extends string,
    To extends string
> = Str extends `${infer Prefix}${From}${infer Suffix}` 
        ? `${Prefix}${To}${Suffix}` : Str;

去除空格:

type TrimStrRight<Str extends string> = 
    Str extends `${infer Rest}${' ' | '\n' | '\t'}` 
        ? TrimStrRight<Rest> : Str;
函数类型

函数类型可以通过模式匹配来获取参数的类型:

type GetParameters<Func extends Function> = Func extends (...args: infer Args) => unknown ? Args : never;
​
type GetParametersResult = GetParameters<(name:'nick') => string>

通过模式匹配获取返回值的类型:

type GetReturnType<Func extends Function> = 
    Func extends (...args: any[]) => infer ReturnType 
        ? ReturnType : never;
​
type GetReturnTypeResult = GetReturnType<() => {name:'nick'}>
重新构造

TypeScript的type、infer、类型参数申明的变量都不能修改,想要产生新的类型就需要用重新构造。

数组类型

向数组末尾添加新的类型:

type Push<Arr extends unknown[], Ele> = [...Arr, Ele];
type PUSHResult = Push<[1,2,3],{name:'nick'}>

或者是向数组前面添加新的类型:

type Push<Arr extends unknown[], Ele> = [Ele,...Arr];
type PUSHResult = Push<[1,2,3],{name:'nick'}>

如果是合并两个数组,可以这样做:

type tuple1 = [1,2];
type tuple2 = ['guang', 'dong'];
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 CapitalizeStr<Str extends string> = 
    Str extends `${infer First}${infer Rest}` 
        ? `${Uppercase<First>}${Rest}` : Str;

字符串下划线转化为驼峰:

type CamelCase<Str extends string> = 
    Str extends `${infer Left}_${infer Right}${infer Rest}`
        ? `${Left}${Uppercase<Right>}${CamelCase<Rest>}`
        : Str;

删除指定字符串:

type DropSubStr<Str extends string, SubStr extends string> = 
    Str extends `${infer Prefix}${SubStr}${infer Suffix}` 
        ? DropSubStr<`${Prefix}${Suffix}`, SubStr> : Str;
函数类型

在已有的函数类型上添加一个参数:

type AppendArgument<Func extends Function, Arg> = 
    Func extends (...args: infer Args) => infer ReturnType 
        ? (...args: [...Args, Arg]) => ReturnType : never;
映射类型

对象、class在TypeScript对应的类型就是索引类型,对索引类型做修改,就要用到映射类型。

接下来对索引类型的值进行更改:

type MapType<T> = {
    [Key in keyof T]: [T[Key], T[Key], T[Key]]
}
​
type res = MapType<{a: 1, b: 2}>;
//
type res = {
    a: [1, 1, 1];
    b: [2, 2, 2];
}

对索引类型的索引进行更改:

type MapType<T> = {
  [Key in keyof T as `${Key & string}${Key & string}${Key & string}`]: [
    T[Key],
    T[Key],
    T[Key]
  ];
};
type res = MapType<{ a: 1; b: 2 }>;
递归复用

递归就是将问题分解为相似的一系列小问题,通过函数调用自身来解决这些问题,直到满足结束条件。TypeScript类型系统不支持循环,当处理数量的个数、长度、层级不确定时,就可以通过递归来处理。

数组类型

反转一个数组,比如type arr = [1,2,3,4,5];,可以这样写:

type ReverseArr<Arr extends unknown[]> = 
    Arr extends [infer First, ...infer Rest] 
        ? [...ReverseArr<Rest>, First] 
        : Arr; 
​
type ReverseArrResult = ReverseArr<[1,2,3,4,5]> //[5.4.3.2.1]

也可以用于查找元素,比如在一个数组type arr = [1,2,3,4,5];中查找等于5,有就返回true,没有就返回false。

type Includes<Arr extends unknown[], FindItem> = 
    Arr extends [infer First, ...infer Rest]
        ? IsEqual<First, FindItem> extends true
            ? true
            : Includes<Rest, FindItem>
        : false;
​
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
type IncludesResult = Includes<[1,2,3,4,5],5> //true

删除指定元素:

type RemoveItem<
    Arr extends unknown[], 
    Item, 
    Result extends unknown[] = []
> = Arr extends [infer First, ...infer Rest]
        ? IsEqual<First, Item> extends true
            ? RemoveItem<Rest, Item, Result>
            : RemoveItem<Rest, Item, [...Result, First]>
        : Result;
        
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
字符串类型

字符串替换:

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 ReverseStr<
    Str extends string, 
    Result extends string = ''
> = Str extends `${infer First}${infer Rest}` 
    ? ReverseStr<Rest, `${First}${Result}`> 
    : Result;
对象类型

对象类型的递归,也可以叫做索引类型的递归。

readonly是一个内置的工具类型,如果要实现的话可以这么做:

type ToReadonly<T> =  {
    readonly [Key in keyof T]: T[Key];
}

如果对象的嵌套层级不确定,就需要用到递归了

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]
}

如果是 object 类型并且还是 Function,那么就直接取之前的值 Obj[Key]。

如果是 object 类型但不是 Function,那就是说也是一个索引类型,就递归处理 DeepReadonly<Obj[Key]>。

数值运算

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

加减乘除
  1. 加法
type BuildArray<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr["length"] extends Length ? Arr : BuildArray<Length, Ele, [...Arr, Ele]>;
​
type Add<Num1 extends number, Num2 extends number> = [
  ...BuildArray<Num1>,
  ...BuildArray<Num2>
]["length"];
​
type result = Add<50, 50>; //100

这里首先定义了一个BuildArray,其中类型参数 Length 是要构造的数组的长度。类型参数 Ele 是数组元素,默认为 unknown。类型参数 Arr 为构造出的数组,默认是 []。 因为数组长度不确定,这里用到了递归, 如果 Arr 的长度到达了 Length,就返回构造出的 Arr,否则继续递归构造。

这里的Add就很简单了,应用了下BuildArray然后返回数组的[length]

  1. 减法

    减法这里的思路就是模式提取,比如 3 是 [unknown, unknown, unknown] 的数组类型,提取出 2 个元素之后,剩下的数组再取 length 就是 1。 确实有点奇技淫巧的感觉。

    type Subtract<
      Num1 extends number,
      Num2 extends number
    > = BuildArray<Num1> extends [...arr1: BuildArray<Num2>, ...arr2: infer Rest]
      ? Rest["length"]
      : never;
    type subResult = Subtract<100, 50>; //50
    
  2. 乘法

    乘法就是多个加法的累加

    type Mutiply<
      Num1 extends number,
      Num2 extends number,
      ResultArr extends unknown[] = []
    > = Num2 extends 0
      ? ResultArr["length"]
      : Mutiply<Num1, Subtract<Num2, 1>, [...BuildArray<Num1>, ...ResultArr]>;
    ​
    type MulResult = Mutiply<10, 10>; //10
           
    

    类型参数 Num1 和 Num2 分别是被加数和加数。

    因为乘法是多个加法结果的累加,我们加了一个类型参数 ResultArr 来保存中间结果,默认值是 [],相当于从 0 开始加。

    每加一次就把 Num2 减一,直到 Num2 为 0,就代表加完了。

    加的过程就是往 ResultArr 数组中放 Num1 个元素。

    这样递归的进行累加,也就是递归的往 ResultArr 中放元素。

    最后取 ResultArr 的 length 就是乘法的结果。

  3. 除法

    除法实际上就是递归的累减

    type Divide<
      Num1 extends number,
      Num2 extends number,
      CountArr extends unknown[] = []
    > = Num1 extends 0
      ? CountArr["length"]
      : Divide<Subtract<Num1, Num2>, Num2, [unknown, ...CountArr]>;
    ​
    type DivResult = Divide<100, 10>; //10
    

    类型参数 Num1 和 Num2 分别是被减数和减数。

    类型参数 CountArr 是用来记录减了几次的累加数组。

    如果 Num1 减到了 0 ,那么这时候减了几次就是除法结果,也就是 CountArr['length']。

    否则继续递归的减,让 Num1 减去 Num2,并且 CountArr 多加一个元素代表又减了一次。

联合类型

当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型。

定义一个联合类型type Union = 'a' | 'b' | 'c';

让这个联合类型其中的a大写,可以这么做

type Union = "a" | "b" | "c";
type UppercaseA<Item extends string> = Item extends "a"
  ? Uppercase<Item>
  : Item;
type Result = UppercaseA<Union>;

这里就不需要递归提取每个元素再处理