TypeScript 类型体操-常见套路篇

TypeScript 类型体操-常见套路篇

套路一: 模式匹配

我们知道,字符串可以和正则做模式匹配,找到匹配的部分,提取子组,之后可以用 $1,$2 等引用匹配的子组。

js 复制代码
'abc'.replace(/a(b)c/, '$1,$1,$1'); // b,b,b

Typescript 的类型也同样可以做模式匹配。

比如这样一个 Promise 类型:

ts 复制代码
type p = Promise<'guang'>;

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

ts 复制代码
type p = Promise<'guang'>;

type GetValueType<P> = P extends Promise<infer Value> ? Value : never;

type res = GetValueType<p>; // guang

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

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

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

数组类型

First

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

ts 复制代码
type getFirst<T extends unknown[]> = T extends [infer F, ...infer R] ? F : never;

type res = getFirst<arr>; // 1
Last

提取数组类型最后一个元素

ts 复制代码
type getLast<T extends unknown[]> = T extends [...infer R, infer L] ? L : never;

type res = getLast<arr>; // 3
Pop

去除数组最后一个元素

ts 复制代码
type Pop<T extends unknown[]> = T extends [...infer R, infer L] ? R : never;

type res = Pop<arr>; // [1, 2]
Shift

去除数组第一个元素

ts 复制代码
type Shift<T extends unknown[]> = T extends [infer R, ...infer L] ? L : never;

type res = Shift<arr>; // [2, 3]

字符串类型

字符串类型也同样可以做模式匹配,匹配一个模式字符串,把需要提取的部分放到 infer 声明的局部变量里。

StartsWith
ts 复制代码
type str = '00-test-end';

type StartsWith<Str extends string, Prefix extends string> = Str extends `${Prefix}${string}` ? true : false;

type res1 = StartsWith<str, '00-'>; // true
type res2 = StartsWith<str, '01-'>; // false
Replace

字符串可以匹配一个模式类型,提取想要的部分,自然也可以用这些再构成一个新的类型。

比如实现字符串替换:

ts 复制代码
type Replace<Str extends string, From extends string, To extends string> = Str extends `${infer Prefix}${From}${infer Suffix}`
  ? `${Prefix}${To}${Suffix}`
  : Str;

type res1 = Replace<'abc', 'a', 'A'>; // Abc
type res2 = Replace<'abc', 'd', 'D'>; // abc
Trim

能够匹配和替换字符串,那也就能实现去掉空白字符的 Trim:

ts 复制代码
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer Rest}` ? TrimLeft<Rest> : S;
type TrimRight<S extends string> = S extends `${infer Rest}${' ' | '\n' | '\t'}` ? TrimRight<Rest> : S;
type Trim<S extends string> = TrimLeft<TrimRight<S>>;

type res1 = TrimLeft<' \n 123  '>; // '123  '
type res2 = TrimRight<'  123  '>; // '  123'
type res3 = Trim<'  123  '>; // '123'

函数

函数同样也可以做类型匹配,比如提取参数、返回值的类型。

GetFunParameters

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

ts 复制代码
type fun1 = (name: string, age: number) => void;

type GetFunParameters<F extends Function> = F extends (...args: infer Args) => unknown ? Args : never;
type res = GetFunParameters<fun1>; // [name: string, age: number]
GetFunReturnType

能提取参数类型,同样也可以提取返回值类型:

ts 复制代码
type fun1<T> = (name: string, age: number) => T;

type GetFunReturnType<F extends Function> = F extends (...args: any[]) => infer ReturnType ? ReturnType : never;

type res = GetFunReturnType<fun1<{ code: number; value: string }>>; // [value: string, code: number]
  • todo
构造器
  • todo
对象

获取对象中一个 key 的值类型

ts 复制代码
type GetValue<Obj, K extends string> = K extends keyof Obj ? Obj[K] : never;

type res = GetValue<{ ref?: 1; value: 2 }, 'ref'>; // 1 | undefined

已知 key 的值,还有一种写法,比如获取 props 对象中的 ref 值

ts 复制代码
type GetRefValue<Obj> = 'ref' extends keyof Obj ? (Obj extends { ref?: infer Value | undefined } ? Value : never) : never;

type res = GetRefValue<{ ref?: 1; value: 2 }>; // 1

套路二: 重新构造做变换

类型编程主要的目的就是对类型做各种转换,那么如何对类型做修改呢?

TypeScript 类型系统支持 3 种可以声明任意类型的变量: type、infer、类型参数。

type 叫做类型别名,其实就是声明一个变量存储某个类型:

ts 复制代码
type ttt = Promise<number>;

infer 用于类型的提取,然后存到一个变量里,相当于局部变量:

ts 复制代码
type GetValueType<P> = P extends Promise<infer Value> ? Value : never;

类型参数用于接受具体的类型,在类型运算中也相当于局部变量:

ts 复制代码
type isTwo<T> = T extends 2 ? true : false;

TypeScript 设计可以做类型编程的类型系统的目的就是为了产生各种复杂的类型,那不能修改怎么产生新类型呢?

TypeScript 的 type、infer、类型参数声明的变量都不能修改,想对类型做各种变换产生新的类型就需要重新构造。

数组类型

Push

实现 push 类型,构造一个新的数组类型

ts 复制代码
type Push<Arr extends unknown[], Ele> = [...Arr, Ele];
type res = Push<[0, 1, 2], 3>; // [0, 1, 2, 3]
Unshift
ts 复制代码
type Unshift<Arr extends unknown[], Ele> = [Ele, ...Arr];
type res = Unshift<[0, 1, 2], 3>; // [3, 0, 1, 2]
Zip

有这样两个元组:

ts 复制代码
type tuple1 = [1, 2];
type tuple2 = ['guang', 'dong'];

我们想把它们合并成这样的元组:

ts 复制代码
type tuple = [[1, 'guang'], [2, 'dong']];

思路很容易想到,提取元组中的两个元素,构造成新的元组:

ts 复制代码
type tuple1 = [1, 2];
type tuple2 = ['guang', 'dong'];

type Zip<Arr1 extends unknown[], Arr2 extends unknown[]> = Arr1 extends [infer Arr1_1, infer Arr1_2]
  ? Arr2 extends [infer Arr2_1, infer Arr2_2]
    ? [[Arr1_1, Arr2_1], [Arr1_2, Arr2_2]]
    : never
  : never;

type res = Zip<tuple1, tuple2>; // [[1, "guang"], [2, "dong"]]

但是这样只能合并两个元素的元组,如果是任意个呢?

那就得用递归了:

ts 复制代码
type tuple1 = [1, 2, 3, 4];
type tuple2 = ['guang', 'dong', 'bei', 'jing'];

type Zip<Arr1 extends unknown[], Arr2 extends unknown[]> = Arr1 extends [infer Arr1_1, ...infer Arr1_Other]
  ? Arr2 extends [infer Arr2_1, ...infer Arr2_Other]
    ? [[Arr1_1, Arr2_1], ...Zip<Arr1_Other, Arr2_Other>]
    : []
  : [];

type res = Zip<tuple1, tuple2>; // [[1, "guang"], [2, "dong"], [3, "bei"], [4, "jing"]]

字符串类型

CapitalizeStr

我们想把一个字符串字面量类型转为首字母大写

ts 复制代码
type CapitalizeStr<Str extends string> = Str extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : '';
type res = CapitalizeStr<'yang'>; // Yang
CamelCase

下划线命名 转 小驼峰命名

ts 复制代码
type CamelCase<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}` ? `${Left}${CapitalizeStr<Right>}${CamelCase<Rest>}` : Str;

type res = CamelCase<'yang_long_hi'>; // yangLongHi
DropSubStr

删除部分字符串

ts 复制代码
type DropSubStr<Str extends string, SubStr extends string> = Str extends `${infer Prefix}${SubStr}${infer Suffix}`
  ? DropSubStr<`${Prefix}${Suffix}`, SubStr>
  : Str;

type res = DropSubStr<'yanglong~~~', '~'>; // yanglong

函数类型

AppendArgument

在参数中添加一个参数

ts 复制代码
type AppendArgument<Fun extends Function, Arg> = Fun extends (...args: infer Args) => infer R ? (...args: [...Args, Arg]) => R : Fun;

type res = AppendArgument<(name: string) => void, number>; // (name: string, args_1: number) => void

对象类型

  • 将所有属性变成可选 和 readonly
ts 复制代码
type obj = {
  name: string;
  age: number;
  gender: boolean;
};
type Mapping<Obj extends object> = {
  readonly [k in keyof Obj]?: Obj[k];
};

type res = Mapping<obj>; // { readonly name?: string | undefined; readonly age?: number | undefined; readonly gender?: boolean | undefined };
  • 将所有属性去除可选和 readonly
ts 复制代码
type obj = {
  readonly name: string;
  readonly age?: number;
  gender?: boolean;
};
type Mapping<Obj extends object> = {
  -readonly [k in keyof Obj]-?: Obj[k];
};

type res = Mapping<obj>; // { name: string; age: number; gender: boolean };
  • 保留指定值类型的 key
ts 复制代码
type obj = {
  name: string;
  age: number;
  gender: boolean;
  hobby: string[];
};
type Mapping<Obj extends Record<string, any>, ValueType> = {
  [K in keyof Obj as Obj[K] extends ValueType ? K : never]: Obj[K];
};

type res = Mapping<obj, string | number>; // { name: string; age: number; };

套路三: 递归复用做循环

会做类型的提取和构造之后,我们已经能写出很多类型编程逻辑了,但是有时候提取或构造的数组元素个数不确定、字符串长度不确定、对象层数不确定。这时候怎么办呢?

其实前面的案例我们已经涉及到了一些,就是递归。

这就是第三个类型体操套路:递归复用做循环。

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

Promise

不确定层数的 Promise 提取类型

ts 复制代码
type ttt = Promise<Promise<Promise<number>>>;
type DeepPromiseValueType<T> = T extends Promise<infer Value> ? DeepPromiseValueType<Value> : T;

type res = DeepPromiseValueType<ttt>; // number

数组类型

ReverseArr

反转数组

ts 复制代码
type ReverseArr<Arr extends unknown[]> = Arr extends [...infer Left, infer Value] ? [Value, ...ReverseArr<Left>] : [];

type res = ReverseArr<[1, 2, 3, 4]>; // [4, 3, 2, 1]
Includes

实现 Includes

ts 复制代码
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
type Includes<Arr extends unknown[], Item> = Arr extends [...infer Left, infer Value]
  ? IsEqual<Value, Item> extends true
    ? true
    : Includes<Left, Item>
  : false;

type res1 = Includes<[1, 2, 3, 4], 5>; // false
type res2 = Includes<[1, 2, 3, 4], 4>; // true
RemoveItem

删除一项

ts 复制代码
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
type RemoveItem<Arr extends unknown[], Item> = Arr extends [...infer Left, infer Value]
  ? IsEqual<Value, Item> extends true
    ? [...Left]
    : [...RemoveItem<Left, Item>, Value]
  : [];

type res1 = RemoveItem<[1, 2, 3, 4], 2>; // [1, 3, 4]
type res2 = RemoveItem<[1, 2, 3, 4], 5>; // [1, 2, 3, 4]

// 另外一种思路
type RemoveItem2<Arr extends unknown[], Item, Res extends unknown[] = []> = Arr extends [infer Value, ...infer Left]
  ? IsEqual<Value, Item> extends true
    ? RemoveItem2<Left, Item, Res>
    : RemoveItem2<Left, Item, [...Res, Value]>
  : Res;
BuildArray

构造数组

ts 复制代码
type BuildArray<Len extends number, Ele = unknown, Arr extends unknown[] = []> = IsEqual<Arr['length'], Len> extends false
  ? BuildArray<Len, Ele, [...Arr, Ele]>
  : Arr;

type res1 = BuildArray<3>; // [unknown, unknown, unknown]

字符串类型

ReplaceAll
ts 复制代码
type ReplaceAll<Str extends string, From extends string, To extends string> = Str extends `${infer Prefix}${From}${infer Suffix}`
  ? ReplaceAll<`${Prefix}${To}${Suffix}`, From, To>
  : Str;

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

type res1 = ReplaceAll<'1,2,3', ',', ''>; // 123
StringToUnion

字符串每一个拆分出来

ts 复制代码
type StringToUnion<Str extends string> = Str extends `${infer First}${infer Res}` ? First | StringToUnion<Res> : never;

type res1 = StringToUnion<'123456'>; // "1" | "2" | "3" | "4" | "5" | "6"
ReverseStr

反转字符串

ts 复制代码
type ReverseStr<Str extends string, Res extends string = ''> = Str extends `${infer First}${infer Last}` ? `${ReverseStr<Last, `${First}${Res}`>}` : Res;

type res1 = ReverseStr<'123456'>; // 654321

对象类型

DeepReadonly
ts 复制代码
type obj1 = {
  a: {
    b: {
      c: {
        f: () => 'dong';
        d: {
          e: {
            guang: string;
          };
        };
      };
    };
  };
};
type DeepReadonly<Obj extends Record<string, any>> = {
  readonly [k in keyof Obj]: Obj[k] extends Object ? DeepReadonly<Obj[k]> : Obj[k];
};

type res1 = DeepReadonly<obj1>;

// 没有计算的key 就不会继续递归
// Obj extends never 或者 Obj extends any 等 触发计算
type DeepReadonly2<Obj extends Record<string, any>> = {
  readonly [k in keyof Obj]: Obj[k] extends any ? (Obj[k] extends Object ? (Obj[k] extends Function ? Obj[k] : DeepReadonly<Obj[k]>) : Obj[k]) : never;
};

套路四: 数组长度做计数

TypeScript 类型系统没有加减乘除运算符,怎么做数值运算呢?

我们构造出来数组类型,那么通过构造不同长度的数组然后取 length,不就是数值的运算么?

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

Add

在处理运算之前我们需要回顾一下 怎么构造一个数组

ts 复制代码
type IsEqual<A, B> = A extends B ? true : false & B extends A ? true : false;
type BuildArray<Length extends number, Ele extends unknown = unknown, Arr extends unknown[] = []> = IsEqual<Arr['length'], Length> extends true
  ? Arr
  : BuildArray<Length, Ele, [...Arr, Ele]>;

ok 现在我们处理一下 add 的实现

ts 复制代码
type IsEqual<A, B> = A extends B ? true : false & B extends A ? true : false;
type BuildArray<Length extends number, Ele extends unknown = unknown, Arr extends unknown[] = []> = IsEqual<Arr['length'], Length> extends true
  ? Arr
  : BuildArray<Length, Ele, [...Arr, Ele]>;

type Add<A extends number, B extends number> = [...BuildArray<A>, ...BuildArray<B>]['length'];
type res = Add<5, 10>; // 15

Subtract

减法是从数值中去掉一部分,很容易想到可以通过数组类型的提取来做。

比如 3 是 [unknown, unknown, unknown] 的数组类型,提取出 2 个元素之后,剩下的数组再取 length 就是 1。

ts 复制代码
type Subtract<A extends number, B extends number> = BuildArray<A> extends [...BuildArray<B>, ...infer Right] ? Right['length'] : never;

type res = Subtract<25, 10>; // 15

Multiply

ts 复制代码
type Multiply<A extends number, B extends number, Res extends unknown[] = []> = B extends 0
  ? Res['length']
  : Multiply<A, Subtract<B, 1>, [...Res, ...BuildArray<A>]>;

type res = Multiply<25, 10>; // 250

乘法就是累加

Divide

ts 复制代码
type Divide<A extends number, B extends number, Res extends unknown[] = []> = A extends 0 ? Res['length'] : Divide<Subtract<A, B>, B, [...Res, unknown]>;

type res = Divide<50, 10>; // 5

除法就是累减

数组长度实现计数

StrLen

数组长度可以取 length 来得到,但是字符串类型不能取 length,所以我们来实现一个求字符串长度的高级类型。

ts 复制代码
type StrLen<Str extends string, Res extends unknown[] = []> = Str extends `${infer First}${infer Other}` ? StrLen<Other, [...Res, unknown]> : Res['length'];

type res = StrLen<'123456'>; // 6
GreaterThan

判断两个数的大小 A > B ? true : false

弄一个数组,累加,看数组的长度先到达哪个,先到的就小

ts 复制代码
type GreaterThan<Num1 extends number, Num2 extends number, Res extends unknown[] = []> = Res['length'] extends Num1
  ? false
  : Res['length'] extends Num2
  ? true
  : GreaterThan<Num1, Num2, [...Res, unknown]>;

type res1 = GreaterThan<15, 3>; // true
type res2 = GreaterThan<3, 13>; // false
Fibonacci

谈到了数值运算,就不得不提起经典的 Fibonacci 数列的计算。

Fibonacci 数列是 1、1、2、3、5、8、13、21、34、...... 这样的数列,有当前的数是前两个数的和的规律。

_F_(0) = 1,_F_(1) = 1, *F*(n) = *F*(n - 1) + *F*(n - 2)(*n* ≥ 2,*n* ∈ N\*)

也就是递归的加法,在 TypeScript 类型编程里用构造数组来实现这种加法:

  • indexArr 索引数组用来计数
  • 每一次 a = b b = a + b
ts 复制代码
type FibonacciLoop<
  aArr extends unknown[] = [],
  bArr extends unknown[] = [],
  indexArr extends unknown[] = [],
  Num extends number = 0
> = indexArr['length'] extends Num ? bArr['length'] : FibonacciLoop<bArr, [...aArr, ...bArr], [unknown, ...indexArr], Num>;

type Fibonacci<Num extends number> = FibonacciLoop<[1], [], [], Num>;

套路五: 联合分散可简化

分布式条件类型

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

ts 复制代码
type Union = 'a' | 'b' | 'c';
// 把联合类型中的 a 大写,就可以这样写:
type UppercaseA<Item extends string> = Item extends 'a' ? Uppercase<Item> : Item;

TypeScript 之所以这样处理联合类型也很容易理解,因为联合类型的每个元素都是互不相关的,不像数组、索引、字符串那样元素之间是有关系的。所以设计成了每一个单独处理,最后合并。

IsUnion

判断联合类型我们会这样写:

ts 复制代码
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;

type res1 = IsUnion<[1, 2, 3]>; // false
type res2 = IsUnion<1 | '1'>; // true

这里的核心逻辑有两个

  • A extends A 我们很好理解, 但是为什么要这样写一下呢
  • ([B] extends [A] ? false : true) 为什么 [B] extends [A] 为真值的时候反而返回 false,表示不是联合类型呢?

我们一步一步来看一下为什么

ts 复制代码
type TestUnion<A, B = A> = { a: A; b: B };

type res = TestUnion<'a' | 'b' | 'c'>;
/**
{
  a: 'a' | 'b' | 'c';
  b: 'a' | 'b' | 'c';
}
 */

我们可以看到这里并没有触发联合类型的分步处理

ts 复制代码
type TestUnion<A, B = A> = A extends A ? { a: A; b: B } : never;

type res = TestUnion<'a' | 'b' | 'c'>;
/**
  { a: 'a'; b: 'a' | 'b' | 'c' } 
| { a: 'b'; b: 'a' | 'b' | 'c' }
| { a: 'c'; b: 'a' | 'b' | 'c' };
 */

我们看到现在触发了联合类型的分布处理,但是只有 A 触发了,B 没有触发,因为触发分布处理的条件是 需要联合类型在条件类型的 extend 关键字的左边

ts 复制代码
type TestUnion<A, B = A> = A extends A ? (B extends B ? { a: A; b: B } : never) : never;

type res = TestUnion<'a' | 'b' | 'c'>;
/**
  { a: 'a'; b: 'a' }
| { a: 'a'; b: 'b' }
| { a: 'a'; b: 'c' }
| { a: 'b'; b: 'a' }
| { a: 'b'; b: 'b' }
| { a: 'b'; b: 'c' }
| { a: 'c'; b: 'a' }
| { a: 'c'; b: 'b' }
| { a: 'c'; b: 'c' };
 */

这样下来,是不是对触发分布计算的条件更加理解了一些呢?

那上面的两个问题我们在来回过头看一下

ts 复制代码
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;
  • A extends A 我们很好理解, 但是为什么要这样写一下呢
    • 是因为要让 A 触发分布计算
  • ([B] extends [A] ? false : true) 为什么 [B] extends [A] 为真值的时候反而返回 false,表示不是联合类型呢?
    • 此时 A 触发了分布计算,B 没有,如果 [B] extends [A] 为真值说明 A 就不是联合类型

BEM

bem 是 css 命名规范,用 block__element--modifier 的形式来描述某个区块下面的某个元素的某个状态的样式。

那么我们可以写这样一个高级类型,传入 block、element、modifier,返回构造出的 class 名:

ts 复制代码
type bemResult = BEM<'guang', ['aaa', 'bbb'], ['warning', 'success']>;

它的实现就是三部分的合并,但传入的是数组,要递归遍历取出每一个元素来和其他部分组合,这样太麻烦了。

而如果是联合类型就不用递归遍历了,因为联合类型遇到字符串也是会单独每个元素单独传入做处理。

数组转联合类型有一种写法

ts 复制代码
type Arr2Union = [1, 2, 3][number]; // 1 | 3 | 2

那么 BEM 就可以这样实现

ts 复制代码
type BEM<block extends string, element extends string[], modifier extends string[]> = `${block}__${element[number]}--${modifier[number]}`;

type bemResult = BEM<'guang', ['aaa', 'bbb'], ['warning', 'success']>;
// "guang__aaa--warning" | "guang__aaa--success" | "guang__bbb--warning" | "guang__bbb--success"

AllCombinations

实现一个全组合

希望传入 'A' | 'B' 的时候,能够返回所有的组合: 'A' | 'B' | 'BA' | 'AB'。

ts 复制代码
type Combination<A extends string, B extends string> = A | B | `${A}${B}` | `${B}${A}`;

那任意多的联合类型的全组合就可以这样写

ts 复制代码
type Combination<A extends string, B extends string> = A | B | `${A}${B}` | `${B}${A}`;

type AllCombinations<A extends string, B extends string = A> = A extends A ? Combination<A, Exclude<B, A>> : never;

type res = AllCombinations<'A' | 'B' | 'C'>;

套路六: 特殊特性要记清

TypeScript 类型系统中有些类型比较特殊,比如 any、never、联合类型,比如 class 有 public、protected、private 的属性,比如索引类型有具体的索引和可索引签名,索引还有可选和非可选。。。

如果给我们一种类型让我们判断是什么类型,应该怎么做呢?

类型的判断要根据它的特性来,比如判断联合类型就要根据它的 distributive 的特性。

我们分别看一下这些特性:

IsAny

如何判断一个类型是 any 类型呢?要根据它的特性来:

any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any。

所以,可以这样写:

ts 复制代码
type IsAny<T> = 2 extends 1 & T ? true : false;

IsEqual

之前我们实现 IsEqual 是这样写的:

ts 复制代码
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);

但是遇到 any 就不好使了,一种能兼容 any 的 IsEqual 写法是这样的

ts 复制代码
type IsEqual2<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
  • 这里利用了泛型函数类型,只有 A,B 可以互相赋值的时候,这个等式才会成立

那这样我们就很容易写出 NotEqual

ts 复制代码
type NotEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? false : true;

IsUnion

利用了 联合类型的分布计算特性

ts 复制代码
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never;

IsNever

never 在条件类型中也比较特殊,如果条件类型左边是类型参数,并且传入的是 never,那么直接返回 never:

ts 复制代码
type TestNever<T> = T extends number ? 1 : 2;
type res = TestNever<never>;
// 返回值是 never,而不是 1 或 2

所以,要判断 never 类型,可以这样写:

ts 复制代码
type IsNever<T> = [T] extends [never] ? true : false;
any 在条件类型中的表现

刚刚提到了 never 在条件类型中的特性,any 在条件类型中也有特性

ts 复制代码
type TestAny<T> = T extends number ? 1 : 2;

// 猜一下会返回什么? 1? 2? any?

// 答案是 1 | 2

如果类型参数为 any,会直接返回 trueType 和 falseType 的合并

IsTuple

元组类型怎么判断呢?它和数组有什么区别呢?

元组类型的 length 是数字字面量,而数组的 length 是 number。

ts 复制代码
type len = [1, 2, 3]['length']; // 3
type len2 = Array<1, 2, 3>['length']; // number

那我们就可以根据这两个特性来判断元组类型:

ts 复制代码
type IsTuple<T> = T extends [...params: infer Eles] ? NotEqual<Eles['length'], number> : false;
// or
type IsTuple2<T extends unknown[]> = NotEqual<T['length'], number>;
  • 先判断 T 是不是一个数组类型
  • 在判断 T 类型的 length 属性

UnionToIntersection

联合类型能不能转交叉类型?

类型之间是有父子关系的,更具体的那个是子类型,比如 A 和 B 的交叉类型 A & B 就是联合类型 A | B 的子类型,因为更具体。

如果允许子类型赋值给父类型,就叫做协变

如果允许父类型赋值给子类型,就叫做逆变

在 TypeScript 中有函数参数是有逆变的性质的,也就是如果参数可能是多个类型,参数类型会变成它们的交叉类型。

ts 复制代码
type UnionToIntersection<U> = (U extends U ? (x: U) => unknown : never) extends (x: infer R) => unknown ? R : never;

type res = UnionToIntersection<{ a: number } | { b: number }>;
// { a: number } & { b: number }

GetOptional

如何提取索引类型中的可选索引呢?

这也要利用可选索引的特性:可选索引的值为 undefined 和值类型的联合类型

过滤可选索引,就要构造一个新的索引类型,过程中做过滤:

ts 复制代码
type GetOptional<Obj extends Record<string, any>> = {
  [Key in keyof Obj as {} extends Pick<Obj, Key> ? Key : never]: Obj[Key];
};

我们来一步步解释这个

  • 这一步我们好理解,用映射类型的语法重新构造索引类型
ts 复制代码
type GetOptional<Obj extends Record<string, any>> = {
  [Key in keyof Obj]: Obj[Key];
};
  • as 之后的部分就是过滤逻辑。 过滤的方式就是单独取出该索引之后,判断空对象是否是其子类型。

这里的 Pick 是 ts 提供的内置高级类型,就是取出某个 Key 构造新的索引类型:

ts 复制代码
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

type res = Pick<obj, 'b'>;
// { b?: string | undefined };

只有对象的 key 是可选的时候, {} extends 的值才是 true

ts 复制代码
type res1 = {} extends { a: number } ? true : false; // false
type res2 = {} extends { b?: number } ? true : false; // true

所以上面的表达式就很好理解了

ts 复制代码
type GetOptional<Obj extends Record<string, any>> = {
  [Key in keyof Obj as {} extends Pick<Obj, Key> ? Key : never]: Obj[Key];
};
  • Key in keyof Obj 用映射类型的语法重新构造索引类型
  • Pick<Obj, Key> 取出当前的 key 为一个对象
  • {} extends Pick<Obj, Key>只有 key 是可选的时候,值才是 true

GetRequired

实现了 GetOptional,那反过来就是 GetRequired,也就是过滤所有非可选的索引构造成新的索引类型:

ts 复制代码
type isRequired<Key extends keyof Obj, Obj> = {} extends Pick<Obj, Key> ? never : Key;

type GetRequired<Obj extends Record<string, any>> = {
  [Key in keyof Obj as isRequired<Key, Obj>]: Obj[Key];
};

RemoveIndexSignature

索引类型可能有索引,也可能有可索引签名。

比如:

ts 复制代码
type Dong = {
  [key: string]: any;
  sleep(): void;
};

这里的 sleep 是具体的索引,[key: string]: any 就是可索引签名,代表可以添加任意个 string 类型的索引。

如果想删除索引类型中的可索引签名呢?

同样根据它的性质,索引签名不能构造成字符串字面量类型,因为它没有名字,而其他索引可以。

所以,就可以这样过滤:

ts 复制代码
type Dong = {
  [key: string]: any;
  sleep(): void;
};

type RemoveIndexSignature<Obj extends Record<string, any>> = {
  [Key in keyof Obj as Key extends `${infer Str}` ? Str : never]: Obj[Key];
};

type res1 = RemoveIndexSignature<Dong>; // { sleep: () => void };

ClassPublicProps

如何过滤出 class 的 public 的属性呢?

也同样是根据它的特性:keyof 只能拿到 class 的 public 索引,private 和 protected 的索引会被忽略

比如这样一个 class:

ts 复制代码
class Dong {
  public name: string;
  protected age: number;
  private hobbies: string[];

  constructor() {
    this.name = 'dong';
    this.age = 20;
    this.hobbies = ['sleep', 'eat'];
  }
}

keyof 拿到的只有 name:

ts 复制代码
type res1 = keyof Dong; // name

所以,我们就可以根据这个特性实现 public 索引的过滤:

ts 复制代码
type ClassPublicProps<Obj extends Record<string, any>> = {
  [Key in keyof Obj]: Obj[Key];
};

as const

TypeScript 默认推导出来的类型并不是字面量类型。

ts 复制代码
// 对象
const obj = { a: 1, b: 2 };
type res = typeof obj;
// { a: number; b: number };

// 数组
const arr = [1, 2, 3];
type res = typeof arr;
// (number)[]

但是类型编程很多时候是需要推导出字面量类型的,这时候就需要用 as const:

ts 复制代码
// 对象
const obj = { a: 1, b: 2 } as const;
type res = typeof obj;
// { readonly a: 1; readonly b: 2; }

// 数组
const arr = [1, 2, 3] as const;
type res = typeof arr;
// readonly [1, 2, 3]

但是加上 as const 之后推导出来的类型是带有 readonly 修饰的字面量类型

相关推荐
跟我很快乐36 分钟前
JavaScript ES6+ 语法速通
javascript·es6
修谦得益36 分钟前
JavaScript是按顺序执行的吗?聊聊JavaScript中的变量提升
javascript·js变量提升·词法环境
Krorainas43 分钟前
将PDF流使用 canvas 绘制然后转为图片展示在页面上(二)
前端·javascript·pdf·react
吉吉安1 小时前
使用echarts实现3d柱状图+折线图
前端·javascript·echarts
CodeToGym1 小时前
React 入门:JSX语法详解
javascript·react.js·ecmascript
苦逼的猿宝1 小时前
React+Antd修改Table组件滚动条样式
前端·javascript·react.js
yqcoder2 小时前
event 用 ts 类型声明
前端·javascript·react.js
java_heartLake2 小时前
Vue3之组件介绍
前端·javascript·vue.js
前端小火龙2 小时前
前端实现扫一扫,扫描二维码(VUE,H5);jsQR,zxing两种方式
前端·javascript·vue.js
北辰浮光2 小时前
[JavaScript]Ajax-异步交互技术
开发语言·javascript·ajax