查看原文
其他

用上这几招,轻松实现 TS 类型提取

semlinker 全栈修仙之路 2021-01-15

阅读须知:本文示例的运行环境是 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
往期精彩回顾
看到 TypeScript 泛型就头晕?给这是我开的方子!
TypeScript 枚举类型
TypeScript 交叉类型

TypeScript never 类型

TypeScript 设计模式之适配器模式

在前端 Word 还能这样玩


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存