各位靓仔靓女们,很高兴今天能和大家聊聊 TypeScript 里的一个相当酷炫的玩意儿——条件类型(Conditional Types)。这玩意儿,听起来高大上,其实就是让 TypeScript 的类型系统也能玩条件判断,就像 if...else
一样。有了它,我们的类型定义就能更加灵活,更加智能,简直是泛型编程的福音!
开场白:TypeScript 类型系统的一点抱怨
说实话,在没遇到条件类型之前,我对 TypeScript 的类型系统是又爱又恨。爱的是它能帮我揪出很多低级错误,恨的是有时候它太死板了,稍微复杂一点的逻辑就搞不定。
比如,我想定义一个函数,如果传入的是字符串,就返回字符串的长度,如果传入的是数字,就返回数字的平方。这在 JavaScript 里简直是小菜一碟,但在 TypeScript 里,如果没有条件类型,就得用各种类型断言或者函数重载,代码一下子就变得臃肿不堪。
// 传统的做法,略显笨拙
function processData(input: string): number;
function processData(input: number): number;
function processData(input: string | number): number {
if (typeof input === 'string') {
return input.length;
} else {
return input * input;
}
}
但是有了条件类型,一切就变得不一样了!
什么是条件类型?
条件类型允许我们根据一个类型表达式的结果来选择不同的类型。它的语法形式长这样:
T extends U ? X : Y
简单解释一下:
T
和U
是类型。extends
关键字表示类型T
是否可以赋值给类型U
,也就是T
是不是U
的子类型。?
和:
就像 JavaScript 里的三元运算符,如果T extends U
的结果是true
,那么类型就是X
,否则就是Y
。
是不是有点像 if...else
?没错,它就是在类型层面上的 if...else
!
条件类型的基本用法:类型推断的利器
让我们用一个简单的例子来演示一下条件类型:
type IsString<T> = T extends string ? true : false;
type StringCheck = IsString<"hello">; // true
type NumberCheck = IsString<123>; // false
在这个例子中,我们定义了一个名为 IsString
的条件类型,它接受一个类型参数 T
。如果 T
是字符串类型,那么 IsString<T>
的类型就是 true
,否则就是 false
。
这看起来好像没什么大不了的,但是,条件类型真正的威力在于它可以和类型推断结合使用。
infer
关键字:类型推断的魔法棒
infer
关键字是条件类型里的一个大杀器。它可以让 TypeScript 在条件判断的过程中自动推断出某个类型。它的语法形式是这样的:
T extends infer R ? X : Y
在这个表达式中,如果 T
可以赋值给某个类型,那么 TypeScript 就会把这个类型推断出来,并且用 R
来表示。然后在 X
类型里,我们就可以使用 R
了。
举个例子,假设我们想从一个函数类型中提取出它的返回值类型。有了 infer
关键字,这简直是易如反掌:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type MyFunc = (a: number, b: string) => boolean;
type MyFuncReturnType = ReturnType<MyFunc>; // boolean
在这个例子中,我们定义了一个名为 ReturnType
的条件类型,它接受一个函数类型 T
。如果 T
是一个函数类型,那么 TypeScript 就会自动推断出它的返回值类型,并且用 R
来表示。然后,ReturnType<T>
的类型就是 R
。
条件类型的高级用法:类型体操的乐园
条件类型不仅可以用来做简单的类型判断,还可以用来做一些非常复杂的类型操作,比如:
- 提取联合类型中的特定类型
- 过滤掉联合类型中的 null 和 undefined
- 实现类型级别的递归
这些操作,我们通常称之为 "类型体操"。听起来很吓人,但其实只要掌握了条件类型和 infer
关键字,你也能轻松玩转类型体操。
实例解析:类型体操实战
咱们来玩几个稍微复杂点的例子,让大家感受一下条件类型的魅力。
1. 提取联合类型中的特定类型
假设我们有一个联合类型 type MyUnion = string | number | boolean
,我们想从中提取出所有的字符串类型。怎么做呢?
type StringInUnion<T> = T extends string ? T : never;
type ExtractString<T> = T extends any ? StringInUnion<T> : never;
type MyUnion = string | number | boolean;
type StringType = ExtractString<MyUnion>; // string
在这个例子中,我们定义了两个条件类型:
StringInUnion<T>
:如果T
是字符串类型,那么返回T
,否则返回never
。never
表示永远不会出现的类型,可以用来从联合类型中排除某个类型。ExtractString<T>
:这个类型利用了分布式条件类型。当T
是一个联合类型时,T extends any ? StringInUnion<T> : never
相当于对联合类型中的每个成员都执行StringInUnion<T>
操作,然后将结果合并成一个新的联合类型。
2. 过滤掉联合类型中的 null
和 undefined
假设我们有一个联合类型 type NullableType = string | null | undefined
,我们想从中过滤掉 null
和 undefined
。怎么做呢?
type NonNullable<T> = T extends null | undefined ? never : T;
type NullableType = string | null | undefined;
type NonNullableStringType = NonNullable<NullableType>; // string
在这个例子中,我们定义了一个名为 NonNullable
的条件类型。如果 T
是 null
或 undefined
,那么返回 never
,否则返回 T
。
3. 实现类型级别的递归(困难警告!)
这个例子比较复杂,需要对条件类型和类型推断有比较深入的理解。
假设我们想定义一个类型,它可以将一个嵌套的数组类型展平。比如,Flatten<string[][]>
的类型应该是 string
,Flatten<number[][][]>
的类型应该是 number
。
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
type NestedArray = string[][][];
type FlattenedArray = Flatten<NestedArray>; // string
这个例子中,我们定义了一个名为 Flatten
的条件类型。如果 T
是一个数组类型,那么 TypeScript 就会自动推断出数组元素的类型,并且用 U
来表示。然后,Flatten<T>
的类型就是 Flatten<U>
,也就是对数组元素的类型递归调用 Flatten
。如果 T
不是一个数组类型,那么 Flatten<T>
的类型就是 T
。
条件类型与泛型编程
条件类型是泛型编程中不可或缺的一部分。它可以让我们根据类型参数的不同来选择不同的类型,从而实现更加灵活和可复用的代码。
例如,我们可以定义一个通用的类型转换函数,它可以将不同的类型转换为字符串类型:
type ToString<T> = T extends string ? T : T extends number ? string : T extends boolean ? string : string;
function toString<T>(value: T): ToString<T> {
if (typeof value === 'string') {
return value as ToString<T>;
} else if (typeof value === 'number') {
return value.toString() as ToString<T>;
} else if (typeof value === 'boolean') {
return value.toString() as ToString<T>;
} else {
return String(value) as ToString<T>;
}
}
const str: string = toString("hello"); // string
const numStr: string = toString(123); // string
const boolStr: string = toString(true); // string
在这个例子中,我们定义了一个名为 ToString
的条件类型,它可以根据类型参数 T
的不同来选择不同的字符串类型。然后,我们定义了一个名为 toString
的泛型函数,它可以接受任何类型的参数,并且返回对应的字符串类型。
一些需要注意的点
在使用条件类型的时候,有一些需要注意的点:
- 分布式条件类型:当
T
是一个联合类型时,T extends U ? X : Y
相当于对联合类型中的每个成员都执行T extends U ? X : Y
操作,然后将结果合并成一个新的联合类型。 never
类型:never
类型表示永远不会出现的类型,可以用来从联合类型中排除某个类型。- 类型推断的局限性:有时候,TypeScript 的类型推断可能无法完全满足我们的需求,我们需要手动指定类型。
总结
条件类型是 TypeScript 类型系统中的一个非常强大的工具。它可以让我们根据类型表达式的结果来选择不同的类型,从而实现更加灵活和智能的类型定义。掌握条件类型,你就能轻松玩转类型体操,写出更加健壮和可维护的 TypeScript 代码。
条件类型,extends
,infer
三者关系总结表
特性 | extends |
infer |
条件类型 ( T extends U ? X : Y ) |
---|---|---|---|
功能 | 类型约束,判断类型关系 | 类型推断,捕获类型信息 | 类型选择,基于条件选择不同类型 |
用法 | T extends U :判断 T 是否可以赋值给 U |
T extends infer R ? ... :从 T 中推断类型 R |
T extends U ? X : Y :如果 T 可以赋值给 U ,则类型为 X ,否则为 Y |
位置 | 类型约束,条件类型 | 条件类型 | 主要组成部分 |
作用对象 | 类型,通常用于泛型约束 | 类型,通常用于函数类型或数组类型等 | 类型,决定最终类型结果 |
示例 | type MyType<T extends string> = ... |
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any |
type IsString<T> = T extends string ? true : false |
关系 | 条件类型依赖 extends 进行判断 |
条件类型可以使用 infer 进行类型推断 |
extends 和 infer 共同构建了条件类型的强大功能 |
结束语
好了,今天的讲座就到这里。希望大家通过今天的学习,能够对条件类型有一个更深入的了解。下次再遇到复杂的类型问题,不妨试试条件类型,说不定会有意想不到的惊喜哦! 记得多练习,熟能生巧!溜了溜了!