用上这几招,轻松实现 TS 类型提取
阅读须知:本文示例的运行环境是 TypeScript 官网的 Playground,对应的编译器版本是 v3.8.3。
一、类型提取
在 TypeScript 中我们能够很方便地从复合类型中提取出单个类型,以数组、元组或对象为例,我们可以通过成员访问的语法来提取数组、元组或对象中元素或属性的类型,具体示例如下:
type Person = {
name: string;
age: number;
}
type PersonName = Person["name"]; // string
type StrNumTuple = [string, number];
type StrNumTuple0 = StrNumTuple[0]; // string
type NumArr = number[];
type NumArrMember = NumArr[0]; // number
对象访问语法同样也适用于接口,使用起来非常直观:
interface Person {
name: string;
age: number;
}
type PersonName = Person["name"]; // string
但是,更有趣的是,我们也可以从泛型和函数中提取类型。假设我们有以下的字典类型:
interface Dictionary<T = any> {
[key: string]: T;
}
type StrDict = Dictionary<string>
为了从 StrDict
类型中提取 T
类型,我们可以使用上面成员属性的方式:
type StrDictMember = StrDict[""]; // string
二、条件类型及 infer
其实除了使用以上的方式外,我们还有另一种选择,就是使用 TypeScript 中的 infer
关键字和条件类型:
type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict>
在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends
关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性。
条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
以上表达式的意思是:若 T
能够赋值给 U
,那么类型是 X
,否则为 Y
。这很好理解,但在 T extends Dictionary<infer V> ? V : never
条件表达式中却多了一个 infer
关键字。在条件类型表达式中,我们可以用 infer
声明一个类型变量并且对它进行使用。
了解完条件类型和 infer
关键字,我们再来看一下完整的代码:
interface Dictionary<T = any> {
[key: string]: T;
}
type StrDict = Dictionary<string>
type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string
除了上述的应用外,利用条件类型和 infer
关键字,我们还可以方便地实现获取 Promise 对象的返回值类型,比如:
async function stringPromise() {
return "Hello, Semlinker!";
}
interface Person {
name: string;
age: number;
}
async function personPromise() {
return { name: "Semlinker", age: 30 } as Person;
}
type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;
type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person
三、ReturnType
TypeScript 官方类型库中提供了 RetrunType
可获取方法的返回类型,其使用示例如下:
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
// Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T6 = ReturnType<string>; // Error
// Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.
type T7 = ReturnType<Function>; // Error
为什么 ReturnType<string>
和 ReturnType<Function>
会抛出上述的异常呢?要解答这个问题,我们就需要来看一下 ReturnType
的定义:
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T
extends (...args: any) => infer R ? R : any;
很明显 ReturnType
内部也是利用条件类型和 infer
关键字,来实现获取方法的返回类型。同理,我们也可以获取参数的类型:
type Fn1 = (a: number) => string;
type ArgType<T> = T extends ((a: (infer U)) => any) ? U : never;
type Fn1Arg = ArgType<Fn1>; // number
如果你想要抽取函数中元组类型的所有参数的类型,这就变得更加有趣,在 TypeScript 3.0 版本之后,元组也支持剩余参数与展开参数,因此我们可以通过定义 ArgsType<T>
类型,来实现上述功能,具体代码如下:
type VariadicFn<A extends any[]> = (...args: A) => any;
type ArgsType<T> = T extends VariadicFn<infer A> ? A : never;
type Fn2 = (a: number, b: string) => string;
type Fn2Args = ArgsType<Fn2>; // [number, string]
infer
关键字除了上述介绍的应用场景之外,它还可以用于实现元组类型转联合类型、联合类型转交叉类型等,这里就不详细展开,大家如果有兴趣的话,可以阅读 深入理解 TypeScript - infer 章节的相关内容。
为了加深大家对 infer
关键字的理解,最后我们再来分析两个简单的示例。
示例一:
type extractArrayType<T> = T extends (infer U)[] ? U : never;
let stringType : extractArrayType<["test"]> = "test";
// Type '"test"' is not assignable to type 'never'.
let stringTypeNoArray : extractArrayType<"test"> = "test"; // Error
在上面代码中,我们使用泛型语法定义了一个名为 extractArrayType
的条件类型,该条件类型会判断是否类型 T 是属于数组类型,如果满足条件的话,我们使用 infer
关键字来声明一个新的类型变量 U 并返回该类型,否则返回 never 类型。这个例子相对比较简单,我们来看一个相对复杂的例子。
示例二:
type InferredAb<T> = T extends { a: infer U, b: infer U } ? U : T;
type abInferredNumber = InferredAb< { a :number, b: number}>;
let abinf : abInferredNumber = 1;
type abInferredNumberString = InferredAb< { a :number, b: string}>;
let abinfstr : abInferredNumberString = 1;
abinfstr = "test";
在上面代码中,我们使用泛型语法定义了一个名为 InferredAb
的条件类型,该条件类型会判断是否类型 T 是否包含 a 和 b 属性,如果满足条件的话,我们使用 infer
关键字来声明一个新的类型变量 U 并返回该类型,否则返回原有的类型 T。
对于 InferredAb<{ a :number, b: number}>
来说,因为 a 和 b 属性的类型都是 number,所以 abInferredNumber
也是 number 类型。但对于 InferredAb< { a :number, b: string}>
来说,
a 属性的类型是 number,这意味着 a: infer U
将返回 number 类型,而 b 属性的类型是 string,这意味着 b: infer U
将返回 string 类型。因此最终的返回类型将会是联合类型,即 number | string
。
四、参考资源
unwrapping-composite-types-in-typescript typescript-how-to-unwrap-remove-promise-from-a-type TypeScript infer 关键字 深入理解 TypeScript - infer