TypeScript 类型体操-常见套路篇
- TypeScript 类型体操-语法入门篇
- 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 修饰的字面量类型