在正式造轮子之前,我们先来熟悉一下即将用到的物料
物料
条件类型
在三元运算的条件判断逻辑中,它主要使用 extends 关键字判断两个类型的子类型关系
type isSubTyping<Child, Par> = Child extends Par ? true : false;
type isAssertable<T, S> = T extends S ? true : S extends T ? true : false;
type isNumAssertable = isAssertable<1, number>; // true
type isStrAssertable = isAssertable<string, 'string'>; // true
type isNotAssertable = isAssertable<1, boolean>; // false
分配条件类型(Distributive Conditional Types)
在条件类型中,如果入参是联合类型,则会被拆解为一个个独立的(原子)类型(成员),然后再进行类型运算。
type BooleanOrString = string | boolean;
type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type WhatIsThis = StringOrNumberArray<BooleanOrString>; // boolean | string[]
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; // string | boolean
同样,通过某些手段强制类型入参被当成一个整体,也可以解除类型分配,如下示例:
type StringOrNumberArray<E> = [E] extends [string | number] ? E[] : E;
type WhatIsThis = StringOrNumberArray<string | boolean>; // string | boolean
注意:包含条件类型的泛型接收 never 作为泛型入参时,存在一定“陷阱”
type GetSNums = never extends number ? number[] : never extends string ? string[] : never; // number[];
type GetNever = StringOrNumberArray<never>; // never
type UsefulNeverX<T> = T extends {} ? T[] : [];
type UselessNeverX<T, S> = S extends {} ? S[] : [];
type UselessNeverY<T, S> = S extends {} ? T[] : [];
type UselessNeverZ<T> = [T] extends [{}] ? T[] : [];
type ThisIsNeverX = UsefulNeverX<never>; // never
type ThisIsNotNeverX = UselessNeverX<never, string>; // string[]
type ThisIsNotNeverY = UselessNeverY<never, string>; // never[]
type ThisIsNotNeverZ = UselessNeverZ<never>; // never[]
条件类型中的类型推断 infer
我们可以在条件类型中使用类型推断操作符 infer 来获取类型入参的组成部分,比如说获取数组类型入参里元素的类型。
{
type ElementTypeOfArray<T> = T extends (infer E)[] ? E : never;
type isNumber = ElementTypeOfArray<number[]>; // number
type isNever = ElementTypeOfArray<number>; // never
}
我们还可以通过 infer 创建任意个类型推断参数,以此获取任意的成员类型,如下示例:
{
type ElementTypeOfObj<T> = T extends { name: infer E; id: infer I } ? [E, I] : never;
type isArray = ElementTypeOfObj<{ name: 'name'; id: 1; age: 30 }>; // ['name', 1]
type isNever = ElementTypeOfObj<number>; // never
}
索引访问类型
索引访问类型其实更像是获取物料的方式,首先我们可以通过属性名、索引、索引签名按需提取对象(接口类型)任意成员的类型(注意:只能使用 [索引名] 的语法)
interface MixedObject {
animal: {
type: 'animal' | 'dog' | 'cat';
age: number;
};
[name: number]: {
type: string;
age: number;
nickname: string;
};
[name: string]: {
type: string;
age: number;
};
}
type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type'];
type numberIndex = MixedObject[number];
type numberIndex0 = MixedObject[0];//我们通过 number 类型索引签名和数字索引 0 获取了第 6~10 行定义的同一个接口类型。
type stringIndex = MixedObject[string];
type stringIndex0 = MixedObject['string'];//我们通过 string 类型索引签名和字符串字面量索引 'string' 获取了第 11~14 行定义的同一个接口类型
keyof
我们还可以使用 keyof 关键字提取对象属性名、索引名、索引签名的类型,如下示例:
//我们使用 keyof 提取了 MixedObject 接口的属性和索引签名,它是由 string、number 和 'animal' 类型组成的联合类型,缩减之后就是 string | number 联合类型
type MixedObjectKeys = keyof MixedObject; // string | number
//我们提取了 'type' 和 'age' 字符串字面量类型组成的联合类型
type animalKeys = keyof animal; // 'type' | 'age'
//我们提取了 'type'、'age' 和 'nickname' 组成的联合类型。
type numberIndexKeys = keyof numberIndex; // "type" | "age" | "nickname"
typeof
如果我们在表达式上下文中使用 typeof,则是用来获取表达式值的类型,如果在类型上下文中使用,则是用来获取变量或者属性的类型。当然,在 TypeScript 中,typeof 的主要用途是在类型上下文中获取变量或者属性的类型,下面我们通过一个具体示例来理解一下。
{
let StrA = 'a';
//typeof 作用在表达式上下文中,获取的是 StrA 值的类型,因为与静态类型上下文无关,所以变量 unions 的类型是 'string'、'number' 等字符串字面量组成的联合类型。
const unions = typeof StrA; // unions 类型是 "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
//typeof 作用在类型上下文中,提取的是变量 StrA 的类型,因为第 1 行推断出来 StrA 的类型是 string,所以提取的类型、变量 str 的类型也是 string。
const str: typeof StrA = 'string'; // strs 类型是 string
type DerivedFromStrA = typeof StrA; // string
//对于任何未显式添加类型注解或值与类型注解一体(比如函数、类)的变量或属性
//我们都可以使用 typeof 提取它们的类型,这是一个十分方便、有用的设计
const animal = {
id: 1,
name: 'animal'
};
type Animal = typeof animal;
const animalFun = () => animal;
type AnimalFun = typeof animalFun;
}
映射类型
我们可以使用索引签名语法和 in 关键字限定对象属性的范围;
我们也可以在映射类型中使用 readonly、? 修饰符来描述属性的可读性、可选性,也可以在修饰符前添加 +、- 前缀表示添加、移除指定修饰符(默认是 +、添加)
注意:in 和 keyof 也只能在类型别名定义中组合使用。
interface SourceInterface {
readonly id: number;
name?: string;
}
type TargetType = {
[key in keyof SourceInterface]: SourceInterface[key];
}; // { readonly id: number; name?: string | undefined }
type TargetGenericType<S> = {
[key in keyof S]: S[key];
};
type TargetInstance = TargetGenericType<SourceInterface>; // { readonly id: number; name?: string | undefined }
使用 as 重新映射 key 自 TypeScript 4.1 起,我们可以在映射类型的索引签名中使用类型断言,如下示例(TypeScript 4.1 以下则会提示错误):
type TargetGenericTypeAssertiony<S> = {
[key in keyof S as Exclude<key, 'id'>]: S[key];
}
// 我们将 key 断言为排除 'id' 以外的联合类型,所以得到的类型是 { name?: string | undefined; }。
type TargetGenericTypeAssertionyInstance = TargetGenericTypeAssertiony<SourceInterface>; // { name?: string | undefined; }
造轮子
我们开始介绍一个自定义工具类型 ReturnTypeOfResolved。
ReturnTypeOfResolved
ReturnTypeOfResolved 和官方 ReturnType 的区别:如果入参 F 的返回类型是泛型 Promise 的实例,则返回 Promise 接收的入参。 我们可以借鉴 ReturnType 的定义实现 ReturnTypeOfResolved,如下示例:
// type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type ReturnTypeOfResolved<F extends (...args: any) => any> = F extends (...args: any[]) => Promise<infer R> ? R : ReturnType<F>;
type isNumber = ReturnTypeOfResolved<() => number>; // number
type isString = ReturnTypeOfResolved<() => Promise<string>>; // string
上面示例中第 1 行注释的代码是官方工具类型 ReturnType 的实现,第 2 行我们自定义了一个泛型 ReturnTypeOfResolved,并约束了入参 F 必须是函数类型。当入参 F 的返回值是 Promise 类型,通过条件类型,我们推断 infer 获取了 Promise 入参类型,所以第 3 行返回的是入参函数返回值类型 number,第 4 行返回的是入参函数返回 Promise 入参类型 string。
Merge
将类型入参 A 和 B 合并为一个类型的泛型 Merge<A, B>
type Merge<A, B> = {
//当 key 为 A、B 的同名属性,合并后的属性类型为联合类型 A[key] | B[key]
[key in keyof A | keyof B]: key extends keyof A ? key extends keyof B ? A[key] | B[key]
: A[key] //当 key是A的属性但不是B的属性时 类型为 A[key]
: key extends keyof B ? B[key] : never; //当 key不是A的属性但是B的属性时 类型为 B[key]
};
type Merged = Merge<{ id: number; name: string }, { id: string; age: number }>;//{ id: number | string; name: string; age: number }
Equal
我们再来实现一个自定义工具类型 Equal<S, T>,它可以用来判断入参 S 和 T 是否是相同的类型。如果相同,则返回布尔字面量类型 true,否则返回 false。
- 我们很容易想到,如果 S 是 T 的子类型且 T 也是 S 的子类型,则说明 S 和 T 是相同的类型,所以 Equal 的实现似乎是这样的:
type EqualV1<S, T> = S extends T ? T extends S ? true : false : false;
type ExampleV11 = EqualV1<1 | number & {}, number>; // true but boolean
type ExampleV12 = EqualV1<never, never>; // true but never
- 此时,我们需要使用 [] 解除条件分配类型和 never “陷阱”,确保自定义泛型仅返回 true 或者 false,所以前面示例的改良版本 EqualV2 如下:
type EqualV2<S, T> = [S] extends [T] ? [T] extends [S] ? true : false : false;
type ExampleV21 = EqualV2<1 | number & {}, number>; // true
type ExampleV22 = EqualV2<never, never>; // true
type ExampleV23 = EqualV2<any, number>; // false but true
- 在示例中的第 2 行、第 3 行,虽然我们解决了联合类型和 never 的问题,但是还是无法区分万金油类型 any 和其他类型。在第 4 行,当入参是 any 和 number,预期应该返回 false,却返回了 true。
这时,我们还需要使用一个可以能识别 any 的改良版 EqualV3 如下
//我们定义了可以区分 any 和其他类型的泛型 IsAny,**因为只有 any 和 1 交叉得到的类型(any)是 0 的父类型,所以如果入参是 any 则会返回 true,否则返回 false**
type IsAny<T> = 0 extends (1 & T) ? true : false;
type EqualV3<S, T> = IsAny<S> extends true
? IsAny<T> extends true
? true
: false
: IsAny<T> extends true
? false
: [S] extends [T]
? [T] extends [S]
? true
: false
: false;
type ExampleV31 = EqualV3<1 | number & {}, number>; // true but false got
type ExampleV32 = EqualV3<never, never>; // true
type ExampleV34 = EqualV3<any, any>; // true
type ExampleV33 = EqualV3<any, number>; // false
type ExampleV35 = EqualV3<never, any>; // false
type ExampleV36 = EqualV3< { name: string }, { readonly name: string }> // true
- 在ExampleV36中如果类型属性包括了修饰符,我们EqualV3并不能很好的判断.为了解决这个问题,我们需要利用TS检查器的特性
条件类型的赋值规则要求在extend关键字后的类型和检查器所定义的类型相同
type EqualV4<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type a = EqualV4< { name: string }, { readonly name: string }> // false
- 但是EqualV4依旧不够完美,存在这样的缺陷
EqualV4<{x: 1} & {y: 2}, {x: 1, y: 2}>为false。似乎目前来讲并没有非常完备的判断方法。也许并不是不存在,毕竟TS图灵完备,但终究需要我们去仔细探索思考研究,才能得出解决方案。
至此,我们造的第一个轮子 Equal(实际上,用来区分 any 类型的泛型 IsAny 也可以算一个轮子)基本可以正确地区分大多数类型了。