TypeScript Utility Types
基于TypeScript 5.0.4, 梳理一下通用的Utility Types, 点评实现原理, 帮助大家加深对TypeScript类型运算的理解
常用类型
Awaited<Type>
拆解Promise类型, 获得Promise的返回值类型, 并且可以递归拆解, 直至返回类型中不再出现Promise类型
使用
type A = Awaited<Promise<string>>; // type A = string
type B = Awaited<Promise<Promise<number>>>; // type B = number
type C = Awaited<boolean | Promise<number>>; // type C = number | boolean源代码
/**
* Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
*/
type Awaited<T> =
T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument
Awaited<V> : // recursively unwrap the value
never : // the argument to `then` was not callable
T; // non-object or non-thenable实现原理
TIP
T是Union类型时, 条件类型推导的结果也会union成一个Union类型
type Once<T> = T extends Array<infer I> ? I : T;
type T = string | Array<number>;
type Item = Once<T>; // string | numberTIP
利用泛型(generics)的递归, 来递归拆解类型
type GetItemValue<T> = T extends Array<infer I> ? GetItemValue<I> : T;
type A1 = string[];
type A2 = string[][];
type A3 = string[][][];
type I1 = GetItemValue<A1>; // string
type I2 = GetItemValue<A2>; // string
type I3 = GetItemValue<A3>; // stringPartial<Type>
将类型的所有属性变为可选
使用
interface Todo {
title: string;
description: string;
}
type PartialTodo = Partial<Todo>;
/*
type PartialTodo = {
title?: string | undefined;
description?: string | undefined;
}
*/源代码
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};实现原理
TIP
利用类型映射(type mapping)可以基于已有类型生成新的类型
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
/*
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/TIP
keyof操作符, 可以获取类型的所有属性名, 且属性名的类型为string | number | symbol
type T = keyof { a: string, b: number, c: boolean }; // type T = "a" | "b" | "c"TIP
keyof操作符还可以和as子句(clause)结合, 重命名属性名
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/Required<Type>
将类型的所有属性变为必需
使用
interface Props {
a?: number;
b?: string;
}
type RequiredProps = Required<Props>;
/*
type RequiredProps = {
a: number;
b: string;
}
*/源代码
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};实现原理
TIP
readonly and ?是两种类型属性修饰符(modifier), 在类型映射(mapping)时用+和-前缀可以增删修饰符, 如果前缀不存在, 默认为+
interface ReadOnlyType {
readonly a: number;
readonly b: string;
}
type WriteableType = {
-readonly [P in keyof ReadOnlyType]: ReadOnlyType[P];
};
/*
type WriteableType = {
a: number;
b: string;
}
*/Readonly<Type>
将类型的所有属性变为只读
使用
interface Todo {
title: string;
description: string;
}
type ReadonlyTodo = Readonly<Todo>;
/*
type ReadonlyTodo = {
readonly title: string;
readonly description: string;
}
*/源代码
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};实现原理
TIP
参考 Required
Record<Keys, Type>
构造一个对象类型, 其属性名为Keys的类型, 属性值为Type的类型
使用
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};源代码
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};实现原理
TIP
keyof any 即 number | string | symbol, 因为只有这三种类型可以作为对象的key
type T = keyof any; // number | string | symbolPick<Type, Keys>
构造一个对象类型, 其属性名来自Keys, 其属性值的类型和Type中的同名属性相同
使用
interface Todo {
readonly title: string;
description?: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, 'title' | 'description'>;
/*
type TodoPreview = {
readonly title: string;
description?: string | undefined;
}
*/源代码
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};实现原理
TIP
K extends keyof T意味着只能使用T中已经存在的属性名
Omit<Type, Keys>
构造一个新类型, 除了Keys中的属性名外, 把Type中的所有属性及类型复制过来
使用
interface Todo {
readonly title: string;
description?: string;
completed: boolean;
}
type TodoTitle = Omit<Todo, 'title' | 'completed'>;
/*
type TodoTitle = {
readonly title: string;
}
*/源代码
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;实现原理
TIP
Omit是Pick和Exclude的组合, 先取出所有属性名, 再排除Keys中的属性名, 最后再从Type中取出剩余的属性名
Exclude<UnionType, ExcludedMembers>
从UnionType中排除ExcludedMembers的类型
使用
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"源代码
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;实现原理
TIP
T extends U ? never : T意味着如果T是U的子类型, 则返回never, 否则返回T
TIP
利用T | never = T 的特性, 可以排除U类型
Extract<Type, Union>
从Type中提取Union的类型, 即Type和Union的交集
使用
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"源代码
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;实现原理
TIP
参考 Exclude
NonNullable<Type>
从Type中排除null和undefined
使用
type T0 = NonNullable<string | number | undefined | null>; // string | number源代码
type NonNullable<T> = T & {};实现原理
TIP
这里着重解释一下&类型操作符, 也就是所谓的交集类型, 很多人最初不理解为啥A和B的交集类型是合并属性, 不是做交集么?
实际上, 从集合论的观点, A & B生成的新类型是A和B的子集, 也就是A和B子类型, 或者说A包含AB且B包含AB;
所以AB extends A AB extends B都是true, 那么当然要把A和B的属性都合并起来, 否则无法满足即是A的子类型, 又是B的子类型;
interface A {
a: string;
}
interface B {
b: string;
}
type AB = A & B;
/*
type AB = {
a: string;
b: string;
}
*/TIP
再看NonNullable的具体实现T & {}, 令:
T = string | number | null | undefined
那么:
T & {} = (string & {}) | (number & {}) | (null & {}) | (undefined & {})
上述运算的结果就是 string | number, 成功剔除null和undefined
TIP
这里利用了{}(empty object type)类型的特殊之处, 所有类型都是{}类型的子类型, void null undefined除外, 并且{}和这三个类型是互斥的, 不存在既是null又是{}(空对象)的类型, 可以参考以下代码
declare const neverValue: never;
declare const voidValue: void;
let v: {} = {};
v = neverValue; // 记住: 右值类型永远是左值类型的子类型!
v = 2;
v = { a: 'c', b: 1 };
v = true;
v = voidValue; // ERROR
v = null; // ERROR
v = undefined; // ERROR
type Never1 = null & {}; // never
type Never2 = undefined & {}; // never
type Never3 = void & {}; // never, 虽然没有代码提示, 但类型为`void`的值是不存在的Parameters<Type>
获取函数类型的参数类型, 返回一个元组类型
使用
type T0 = Parameters<(a: string, b: number) => string>; // [string, number]源代码
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;实现原理
TIP
(...args: any) => any表示任意函数类型
type F = (...args: any) => any;
let f: F = () => {};
f = (a: string) => a;
f = (a: string, b: number) => true;TIP
T extends (...args: infer P)使用infer子句可以推导T的参数类型
ConstructorParameters<Type>
获取构造函数类型的参数类型, 返回一个元组类型
使用
type T0 = ConstructorParameters<ErrorConstructor>; // [message?: string]
type T1 = ConstructorParameters<FunctionConstructor>; // string[]
type T2 = ConstructorParameters<RegExpConstructor>; // [pattern: string | RegExp, flags?: string]
type T3 = ConstructorParameters<any>; // unknown[]源代码
/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;实现原理
TIP
new (...args: any) => any表示任意构造函数类型, 类似Java中的Class类, 也就是class类型, 这里加上abstract是为了包含abstract class, 保证ConstructorParameters也能用于获取抽象类的构造器参数
实际上, new (...args: any) => any是abstract new (...args: any) => any的子类型
type BF = abstract new (...args: any) => any;
type F = new (...args: any) => any;
abstract class A {}
class B {}
let f: F;
f = B;
f = A; // ERROR
f = function() {}; // ERROR
let bf: BF;
bf = B;
bf = A;
bf = function() {}; // ERRORReturnType<Type>
获取函数类型的返回值类型
使用
type T0 = ReturnType<() => string>; // string源代码
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;实现原理
TIP
参考 Parameters
InstanceType<Type>
获取构造函数类型的实例类型, 其在类似反射的场景中非常有用
使用
class A {}
type AConstructor = typeof A;
type T0 = InstanceType<AConstructor>; // A
const o: T0 = new A();function createInstance<T extends new (...args: any) => any>(clazz: T): InstanceType<T> {
return new clazz();
}
class A {}
const instance = createInstance(A);源代码
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;实现原理
TIP
参考 ReturnType
ThisParameterType<Type>
获取函数类型的this类型, 其在一些通过函数共享代码逻辑的场景中非常有用
使用
首先, 我们定义一个函数, 仅能在Number类型上调用, 代码如下:
function toHex(this: Number) {
return this.toString(16);
}然后, 我们利用ThisParameterType可以限制toHex的调用者类型, 代码如下:
function numberToString(n: ThisParameterType<typeof toHex>) {
return toHex.apply(n);
}源代码
/**
* Extracts the type of the 'this' parameter of a function type, or 'unknown' if the function type has no 'this' parameter.
*/
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any ? U : unknown;实现原理
TIP
参考 Parameters
OmitThisParameter<Type>
把this参数类型从函数类型中去除
使用
function toHex(this: Number) {
return this.toString(16);
}
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
console.log(fiveToHex());源代码
/**
* Removes the 'this' parameter from a function type.
*/
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;实现原理
TIP
注意条件类型的写法unknown extends ThisParameterType<T>, 结合ThisParameterType的实现代码, 可以理解为ThisParameterType返回unknown时, 表示T不是函数类型, 直接返回T, 否则返回函数类型的参数类型和返回值类型
WARNING
以上写法为什么不能写成ThisParameterType<T> extends unknown呢?
因为A extends B表示A是B的子类型, 在TypeScript中, unknown和any是top type, 即所有其他类型都是这两个类型的子类型, 所以ThisParameterType<T> extends unknown永远为true
TIP
any和unknown都是top type, unknown是更安全版本的any, 而类型声明为top type的变量, 可以把任何类型的值赋给它
但是any类型的值也可以赋给任何其他类型, 除了never
declare const anyValue: any;
let num: number = anyValue;
let str: string = anyValue;
let unknownValue: unknown = anyValue;
let nullValue: null = anyValue;
let undefinedValue: undefined = anyValue;
let voidValue: void = anyValue;
let neverValue: never = anyValue; // ERRORTIP
never则是bottom type, 也就是说never是任何类型的子类型
declare const neverValue: never;
let num: number = neverValue;
let str: string = neverValue;
let anyValue: any = neverValue;
let unknownValue: unknown = neverValue;
let nullValue: null = neverValue;
let undefinedValue: undefined = neverValue;
let voidValue: void = neverValue;WARNING
理论上T extends never永远为false, 但是TypeScript中有一些特例:
type Never<T> = T extends never ? true : false;
type Number1 = Never<boolean>; // false
type Number2 = Never<string>; // false
type Number3 = Never<void>; // false
type Number4 = Never<unknown>; // false
type Number5 = Never<{}>; // false
type NumberOrString = Never<any>; // false | true = boolean
type Never1 = Never<never>; // neverDANGER
这里不再继续展开讨论, 但是要注意, any unknown never 这三个类型出现在条件类型中 extends 的左右两边时, 会有很多奇异的行为!!!!
ThisType<Type>
ThisType实际上是一个interface, 用于标记this类型
使用
WARNING
开启 noImplicitThis 编译选项后, this类型不再会被推断为any类型, 此时需要使用ThisType标记this类型
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}
let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this
},
},
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);源代码
/**
* Marker for contextual 'this' type
*/
interface ThisType<T> { }实现原理
TIP
空白的接口, 用于标记this类型
Intrinsic String Manipulation Types
TypeScript提供了一些内置的字符串操作类型, 用于操作字符串类型, 由tsc编译器内部实现(intrinsic)