类型物料
泛型
工具类型的本质就是构造复杂类型的泛型。
type isXX = 1 extends number ? true : false;
type isYY = 'string' extends string ? true : false;
这是使用了extends关键字和type类型别名返回了布尔字面量true和false, 这是一种效率低下的做法, 因为不能把其中的逻辑复用在对其他类型子类型关系的判断上。 此时,我们需要吧确切的类型抽离为入参, 然后封装成一个可复用的泛型。
type isSubTyping<Child, Parent> = Child extends Parent ? true : false;
type isXX2 = isSubTyping<1, number>; // true
type isYY2 = isSubTyping<'string', number>; // false
type isZZ2 = isSubTyping<true, boolean>; // true
条件类型
TypeScript支持使用三元运算的条件类型, 它可以根据 ? 前面的条件判断返回不同的类型。三元运算支持嵌套。
三元运算主要使用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
利用extends关键字判断入参T是否是S的子类型或S是T的子类型, 从而判断它们之间是否可断言关系。
分配条件类型 Distributive Conditional Types
条件类型中, 如果入参时联合类型, 则会被拆解为一个个独立的(原子)类型(成员),然后再进行类型运算。
type BooleanOrString = string | boolean;
type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type WhatIsThis = StringOrNumberArray<BooleanOrString>; // string[] | boolean;
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; // string | boolean;
string和boolean组成的联合类型BooleanOrString作为泛型StringOrNumberArray入参时, 会被拆解成string和boolean两个独立的类型, 再通过extends关键字判断是否是string|number类型的子类型。
可以使用[]包括传参来强制类型传参被当成一个整体,解除类型分配。
type StringOrNumberArray<E> = [E] extends [string | number]: E[] : E;
type WhatIsThis = StringOrNumberArray<string | boolean>; // string | boolean;
never 陷阱
注意, 包含条件类型的泛型接收never作为泛型入参时,存在一定“陷阱”, 如下:
type GetSNums = never extends number
? number[]
: never extends string
? string[]
: never; // number[]
type GetNever = StringOrNumberArray<never>; // never
因为never是所有类型的子类型, 自然也是number的子类型,所以返回的是number类型的数组;第二行传入never作为入参来实例化前面定义的典型StringOrNumberArray时,返回的类型却是never.
泛型StringOrNumberArray的实现与示例第一行=右侧的逻辑并没有任何区别(除never被抽离成入参之外), 这是因为never是不能分配的底层类型, 如果 作为入参 以原子形式出现在条件判断extends关键字左侧,则实例化得到的类型也是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 = UsefulNever<never>; // never
type ThisIsNotNeverX = UselessNeverX<never, string>; // string[];
type ThisIsNotNeverY = UselessNeverY<never, string>; // never[];
type ThisIsNotNeverZ = UselessNeverZ<bever>; // never[]
泛型UsefulNeverX的入参T被三元运算中的extends使用,所以UsefulNeverX<never>返回never. 后面两个因为没有用到入参T, 所以返回never[], 最后一行入参T是以T[] 而不是以原型形式被extends使用, 所以返回never[]。 综上,never作为泛型传参, 返回never的前提条件:
- 作为泛型入参
- 原子形式被使用(出现在extends左侧, 独立使用, T[]不属于原子形式)
条件类型中的类型推断infer
我们可以在条件类型中使用类型推断操作符infer来获取类型入参的组成部分, 比如获取数组类型入参里的元素类型。 TODO?????
type ElementTypeOfArray<T> = T extends (infer E)[] ? E[] : never;
type isNumber = ElementTypeOfArray<number[]>; // number
type isNever = ElementTypeOfArray<number>; // never
示例中, 我们定义了接收入参T的泛型ElementTypeOfArray, 并在三元运算符的条件判断中, 通过(infer E)[]定义了一个有对元素类型推断参数E的数组, 当入参T满足是(infer E)[]数组类型的子类型的条件, 则返回参数E,及数组元素类型, 所以传入number[] 返回number, 传入number返回never。
我们还可以通过infer创建人一个类型推断参数, 以此获取任意的成员类型。
type ElementTypeOfObj<T> = T extends {name: infer E, id: infer I} ? [E, I] : never;
type isArray = ElementTypeOfObj<{name: 'name', id: 1}>; // ['name', 1]
type isNever = ElementTypeOfObj<number>; // never
定义了入参是 T 的泛型 ElementTypeOfObj,并通过两个 infer 类型推断来获取入参 name、id 属性的类型。在第 3 行,因为入参是包含 name、id 属性的接口类型,所以提取到了元组类型 ['name', 1]。而在第 4 行,因为入参 number 不满足三元运算中的条件判断,所以返回了 never。
索引访问类型
索引访问类型其实更像是获取物料的方式,首先我们可以通过属性名,索引,索引签名按需提取对象(接口类型)任意成员的类型(注意:只能使用【索引名】的语法)。如下:
inferface MixedObject {
animal: {
type: 'animal' | 'dig' | 'cat',
age: number
};
// 属性约束 key为number类型的值取值
[name: number]: {
type: string;
age: number;
nickname: string;
};
// 属性约束, key为string类型的值取值类型
[name: string]: {
type: string;
age: number
}
}
type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type']
// 等效
type numberIndex = MixedObject[number]
type numberIndex0 = MixedObject[0];
// 等效
type stringIndex = MixedObject[string];
type stringIndex0 = MixedObject['string'];
传送门: 接口类型-索引签名
keyof
可以使用keyof关键字提取对象属性名,索引名,索引签名的类型。 返回联合类型
type MixedObjectKeys = keyof MixedObject; // string | number
type animalKeys = keyof animal; // 'type' | 'age'
type numberIndexKeys = keyof numberIndex; // 'type' | 'age' | 'nickname'
typeof
在表达式上下文中使用typeof,是用来获取表达式值的类型, 如果在类型上下文中使用,则是用来获取变量或者属性的类型。 在TypeScript中, typeof的主要用途是在类型上下文中获取变量或者属性的类型。
let StrA = 'a'
// 表达式中使用
const unions = typeof StrA; // "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
// 类型上下文中使用
const str: type StrA = '123' // 'string'
// 使用类型别名提取类型
type DerivedFromStrA = typeof StrA; // 'string'
注意区分表达式上下文(js场景,获取的是变量值的类型,所以是'string' | 'number' | ... 组成的联合类型)和类型上下文(ts环境,获取的是变量的类型)
对于任何未显示添加类型注解或值与类型注解一体(比如函数,类)的变量或者属性, 我们可以使用typeof提取他们的类型。
const animal = {
id: 1,
name: 'animal'
}
type Animal = typeof animal;
const animalFun = () => animal
type AnimalFun = typeof animalFun;
映射类型
我们可以使用索引签名语法和in关键字限定对象属性的范围。 如:
type SpecifiedKeys = 'id' | 'name';
type TargetType = {
[key in SpecifiedKeys]: unknown;
}
type TargetGeneric<O extends string | number | symbok> = {
[key in O]: unknown;
}
type TargetInstance = TargetGeneric<SpecifiedKeys>; // {id: unknown; name: unknown}
in,keyof 只能在类型别名中使用,在接口中使用一般使用具体的类型
interface SourceInterface {
readonly id: number;
name?: string; // 可缺省
}
type TargetType = {
[key in keyof SourceInterface]: SourceInterface[key]
}
type TargetGenericType<S> = {
[key in keyof S]: S[key]
}
type TargetInstance = TargetGeneric<SourceInterface>;
// {readonly id: number; name?: string | undefined }
同样, 可以在映射类型中使用readonly,?修饰符来描述属性的可读性,可选性。也可以在修饰符前添加+,-前缀表示添加, 移除指定修饰符。
type TargetGenericTypeReadonly<S> = {
readonly [key in keyof S]: S[key]
}
// {readonly id: number; readonly name?: string}
type TargetGenericTypeReadonlyInstance = TargetGenericTypeReadonly<SourceInterface>;
type TargetGenericTypeOptional<S> = {
[K in keyof S]?: S[K]
}
type TargetGenericTypeOptionalInstance = TargetGenericTypeOptional<SourceInterface>;
// {readonly id?: number; name?: string | undefined; }
type TargetGenericTypeRemoveReadonly<S> = {
-readonly [K in keyof S]: S[K]
}
type TargetGenericTypeRemoveReadonlyInstace = TargetGenericTypeRemoveReadonly<SourceInterface>;
// {readonly id: number; readonly name?: string | undefined;}
type TargetGenericTypeRemoveOptional<S> = {
[K in keyof S]-?: S[K]
}
type TargetGenericTypeRemoveOptionalInstance = TargetGenericTypeRemoveOpional<SourceInterface>;
// {readonly id: number; name: string}
使用as重新映射key, 自TypeScript 4.1起,我们可以在映射类型的索引签名中使用类型断言。
type TargetGenericTypeAssertion<S> = {
[K in keyof S as Exclude<K, 'id'>]: S[K]
}
type TargetGenericTypeAssertionInstance = TargetGenericTypeAssertion<SourceInterface>;
// {name?: string | undefined;}
造轮子
Exclude
先看看Exclude使用实例
type ExcludeSpecifiedNumber = Exclude<1 | 2, 1>; // 2
type ExcludeSpecifiedString = Exclude<'id' | 'name', 'id'>; // name
type ExcludeSpecifiedBoolean = Exclude<boolean, true>; // false
再来看看Exclude实现源码
type Exclude<T, U> = T extends U : never : T;
这里就是利用了在类型别名中使用泛型传参的分配条件类型特性,入参T会被拆解为成员类型。 如果成员类型时入参T的子类型, 则返回never,否则返回成员类型。
ReturnTypeOfResolved
ReturnType: 返回函数的返回类型 ReturnTypeOfResolved: 如果入参F的返回类型时泛型Promise的实例, 则返回Promise接收的入参。
type Reutrn<F extends (...args) => any> = F extends (...args: any[]) => infer T ? T : 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
Merge
合并两个类型,如果有相同名称属性,但是类型不同,则进行类型联合
type Merge<A, B> = {
[key in keyof A | keyof B]:
key extends keyofA
? key extends keyofB
? A[key] | B[key]
: A[key]
: key extends keyof[B]
? B[key]
: never;
}
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
需要注意点:
- never类型陷阱(使用
[]解除条件分配和类型陷阱) - any既是所有类型的子类型, 也是所有类型的父类型(使用isAny函数来判断是否是any)
type IsAny<T> = 0 extends (1 & T) ? true : false;
type isEqual<S, T> = isEqual<S> extends true
? isEequal<T> extends true
? true
: false
isEqual<T> extends true
? false
: [S] extends [T]
? [T] extends [S]
? true
: false
: false;
知识点: 只有any满足与任何类型交叉得到的都是any,而any既是所有类型的父类型, 也是所有类型的子类型。