用了 TS 条件类型,同事直呼 YYDS!

开发 前端
你用过 Exclude、Extract、NonNullable、Parameters 和 ReturnType 这些工具类型吗?

阿宝哥精心准备的《轻松学 TypeScript》 视频教程已经更新到第十期了,通过形象生动的动画,让你轻松搞懂 TypeScript 的难点和核心知识点!

你用过 Exclude、Extract、NonNullable、Parameters 和 ReturnType 这些工具类型吗?

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T extends null | undefined ? never : T;
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

你知道它们内部是如何工作的吗?如果你想彻底掌握它们且实现自己的工具类型,那么本文千万不要错过。上面看到的那些内置工具类型,它们内部使用了 TypeScript 2.8 版本引入的条件类型(Conditional Types)。

该类型的语法如下:

T extends U ? X : Y

其中 T、U、X 和 Y 这些都是类型占位符。你可以这样理解该语法,当类型 T 可以赋值给类型 U 时,那么返回类型 X,否则返回类型 Y。

看到这里你是不是也想到了 JavaScript 中的三元表达式。那么条件类型有什么用呢?这里我们来举个例子:

type IsString<T> = T extends string ? true : false;
type I0 = IsString<number>; // false
type I1 = IsString<"abc">; // true
type I2 = IsString<any>; // boolean
type I3 = IsString<never>; // never

在以上代码中,我们定义了 IsString 工具类型。使用该工具类型,我们可以判断传给类型参数 T 的实际类型是否为字符串类型。除了判断单一类型之外,利用条件类型和条件链,我们还可以同时判断多种类型。

下面我们来看一下如何实现该功能:

type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"

在以上代码中,我们定义了一个新的 TypeName 工具类型,在该工具类型中,我们使用了条件链。为了便于大家理解条件链,我们以 JavaScript 三元表达式为例,来演示一下它的作用。

function example() {
return condition1 ? value1
: condition2 ? value2
: condition3 ? value3
: value4;
}
// 等价于
function example() {
if (condition1) { return value1; }
else if (condition2) { return value2; }
else if (condition3) { return value3; }
else { return value4; }
}

现在问题来了,对于前面定义的 TypeName 工具类型来说,如果传入的类型是联合类型的话,那么将返回什么结果?下面我们来验证一下:

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T11 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"

为什么 T10 和 T11 类型返回的是联合类型呢?这是因为 TypeName 属于分布式条件类型。在条件类型中,如果被检查的类型是一个 “裸” 类型参数,即没有被数组、元组或 Promise 等包装过,则该条件类型被称为分布式条件类型。对于分布式条件类型来说,当传入的被检查类型是联合类型的话,在运算过程中会被分解成多个分支。

T extends U ? X : Y 
T => A | B | C
A | B | C extends U ? X : Y =>
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

为了便于大家理解,我们来举个例子:

type Naked<T> = T extends boolean ? "Y" : "N";
type WrappedTuple<T> = [T] extends [boolean] ? "Y" : "N";
type WrappedArray<T> = T[] extends boolean[] ? "Y" : "N";
type WrappedPromise<T> = Promise<T> extends Promise<boolean> ? "Y" : "N";
type T0 = Naked<number | boolean>; // "N" | "Y"
type T1 = WrappedTuple<number | boolean>; // "N"
type T2 = WrappedArray<number | boolean>; // "N"
type T3 = WrappedPromise<number | boolean>; // "N"

由以上结果可知,如果条件类型中的类型参数 T 被包装过,该条件类型就不属于分布式条件类型,所以在运算过程中就不会被分解成多个分支。

了解完条件类型和分布式条件类型的知识点,我们来举例演示一下 TypeScript 内置工具类型 Exclude 的执行流程。

type Exclude<T, U> = T extends U ? never : T;
type T4 = Exclude<"a" | "b" | "c", "a" | "b">
("a" extends "a" | "b" ? never : "a") // => never
| ("b" extends "a" | "b" ? never : "b") // => never
| ("c" extends "a" | "b" ? never : "c") // => "c"
never | never | "c" // => "c"

掌握了条件类型之后,再结合往期文章中介绍的映射类型,我们就可以实现一些有用的工具类型。比如实现 FunctionProperties 和 NonFunctionProperties 等工具类型。

type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface User {
id: number;
name: string;
age: number;
updateName(newName: string): void;
}
type T5 = FunctionPropertyNames<User>; // "updateName"
type T6 = FunctionProperties<User>; // { updateName: (newName: string) => void; }
type T7 = NonFunctionPropertyNames<User>; // "id" | "name" | "age"
type T8 = NonFunctionProperties<User>; // { id: number; name: string; age: number; }

在以上代码中,利用上述的工具类型,我们就可以轻松地提取 User 对象类型中函数类型和非函数类型的属性及相关的对象类型。

责任编辑:姜华 来源: 全栈修仙之路
相关推荐

2022-04-29 06:54:48

TS 映射类型User 类型

2022-06-08 08:01:28

模板字面量类型

2020-09-27 10:55:10

代码Java字符串

2022-05-04 23:09:33

TS交叉运算工具

2022-12-20 08:32:02

2020-04-30 10:07:54

数据库数据迁移Insert into

2020-09-24 11:10:05

Python代码字符串

2022-01-19 23:41:56

TS索引类型

2022-02-12 22:16:53

TypeScript类型字符串

2022-02-25 14:04:56

TS前端代码

2022-08-08 23:49:01

TypeScriptumd模块

2022-12-30 08:08:30

2020-09-04 15:13:43

Java 8接口物流信息

2022-08-14 06:59:01

TS 类型编程索引类型

2022-11-13 08:06:05

ArthasEverythingMaven help

2021-09-02 08:24:41

TypeScript 泛型前端

2022-04-17 10:29:10

TSTypeScript对象类型

2021-04-20 23:44:25

Excel工具Java

2021-08-19 08:31:46

云计算

2021-12-25 22:29:31

类型编程Javascript类型体操
点赞
收藏

51CTO技术栈公众号