简介
TypeScript 中的条件类型允许您在类型级别使用一种三元逻辑形式来表达类型关系。它们提供了一种执行类型检查并根据这些检查返回不同类型的方法。条件类型的语法和基本用法类似于 JavaScript 的三元运算符。
条件类型有助于描述输入和输出类型之间的关系。
T extends U? X : Y
先决条件
-
JavaScript
您将学到
-
一些条件类型示例
-
条件类型约束
-
条件类型中的推断
-
分布式条件类型
一些条件类型示例
条件类型采用的形式有点类似于 JavaScript 中的条件表达式(条件? 真表达式 : 假表达式):
SomeType extends OtherType? TrueType : FalseType;
当extends
左边的类型可以赋值给右边的类型时,您将获得第一个分支(“真”分支)中的类型;否则,您将获得后一个分支(“假”分支)中的类型。
让我们为createLabel
函数再举一个示例:
interface IdLabel { id: number /* 一些字段 */; } interface NameLabel { name: string /* 其他字段 */; } function createLabel(id: number): IdLabel; function createLabel(name: string): NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel { throw "未实现"; }
这些createLabel
的重载描述了一个基于输入类型进行选择的单个 JavaScript 函数。请注意以下几点:
-
如果库在其整个 API 中必须一遍又一遍地进行相同类型的选择,这会变得很麻烦。
-
我们必须创建三个重载:每个确定类型的情况(一个用于字符串,一个用于数字),以及一个用于最通用的情况(接受
string | number
)。对于createLabel
可以处理的每个新类型,重载的数量呈指数增长。
相反,我们可以在条件类型中编码该逻辑:
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
然后,我们可以使用该条件类型将重载简化为一个没有重载的函数。
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { throw "未实现"; } let a = createLabel("typescript"); let a: NameLabel let b = createLabel(2.8); let b: IdLabel let c = createLabel(Math.random()? "hello" : 42); let c: NameLabel | IdLabel
条件类型约束
在 TypeScript 中,条件类型约束是使用条件类型在泛型类型上对类型参数施加限制(约束)的一种方式。此技术允许您表达更复杂的类型关系,并确保类型参数满足某些条件。
让我们从一个基本示例开始说明条件类型约束的工作方式:
type CheckType<T> = T extends string? "String Type" : "Other Type"; type A = CheckType<string>; // "String Type" type B = CheckType<number>; // "Other Type"
在这个示例中,CheckType
是一个条件类型,如果类型T
扩展自字符串,则返回"String Type";否则,返回"Other Type"。
在泛型中使用条件类型约束
条件类型约束通常用于泛型类型中以实施更具体的类型规则。
示例:约束函数参数
以下是一个条件类型约束确保函数参数必须是特定类型的示例:
type EnsureString<T> = T extends string? T : never; function logString<T>(value: EnsureString<T>): void { console.log(value); } logString("Hello"); // 有效 logString(42); // 错误:类型'42'的参数不能赋给类型'never'的参数。
在这个示例中,EnsureString
类型确保T
必须是字符串。如果T
不是字符串,类型变为never
,这是一个不可用的类型,并导致类型错误。
更复杂的示例:基于对象属性进行约束
您还可以使用条件类型约束来实施更复杂的规则,例如确保对象具有特定的属性:
type HasName<T> = T extends { name: string }? T : never; function greet<T>(obj: HasName<T>): void { console.log(`Hello, ${obj.name}`); } greet({ name: "Alice" }); // 有效 greet({ name: "Bob", age: 30 }); // 有效 greet({ age: 30 }); // 错误:类型'{ age: number; }'的参数不能赋给类型'never'的参数。
在这个示例中,HasName
类型确保T
必须是具有name
属性且类型为字符串的对象。如果T
没有此属性,类型变为never
,导致类型错误。
条件类型中的推断
在 TypeScript 中,您可以在条件类型中使用infer
关键字引入一个类型变量,该变量可以从类型的特定部分推断出来。当处理复杂类型(如函数、元组和数组)时,这特别有用,您可能希望提取特定类型的组件。
在条件类型中使用infer
的语法如下:
type SomeType<T> = T extends SomeCondition? infer U : FallbackType;
在这里,如果T
满足SomeCondition
,infer U
允许 TypeScript 从T
中推断出类型U
。
示例
提取函数的返回类型
您可以使用infer
来提取函数的返回类型:
type GetReturnType<T> = T extends (...args: any[]) => infer R? R : never; type Func = (a: number, b: string) => boolean; type ReturnTypeOfFunc = GetReturnType<Func>; // boolean
在这个示例中,GetReturnType
检查T
是否为函数类型。如果是,则infer R
捕获函数的返回类型,GetReturnType
解析为该返回类型(R
)。否则,它解析为never
。
提取数组的元素类型
您可以使用infer
来提取数组的元素类型:
type ElementType<T> = T extends (infer U)[]? U : never; type ArrayOfNumbers = number[]; type NumberElementType = ElementType<ArrayOfNumbers>; // number
在这里,ElementType
检查T
是否为数组类型。如果是,则infer U
捕获元素类型,ElementType
解析为该元素类型(U
)。否则,它解析为never
。
提取元组类型
您可以使用infer
来处理元组类型并提取其组件类型:
type FirstElement<T> = T extends [infer U,...any[]]? U : never; type Tuple = [string, number, boolean]; type FirstType = FirstElement<Tuple>; // string
在这个示例中,FirstElement
检查T
是否为元组类型。如果是,则infer U
捕获第一个元素的类型,FirstElement
解析为该类型(U
)。否则,它解析为never
。
高级示例:提取函数的参数类型
您可以使用infer
来提取函数的参数类型:
type GetParameters<T> = T extends (...args: infer P) => any? P : never; type Func = (a: number, b: string) => void; type ParametersOfFunc = GetParameters<Func>; // [number, string]
在这个示例中,GetParameters
检查T
是否为函数类型。如果是,则infer P
捕获参数类型作为元组,GetParameters
解析为该元组类型(P
)。否则,它解析为never
。
分布式条件类型
当条件类型作用于泛型类型并且给定联合类型时,它们会变为分布式。这意味着如果您有一个条件类型T
并且T
是联合类型,则条件类型将应用于联合的每个成员。
例如,考虑以下内容:
type ToArray<Type> = Type extends any? Type[] : never;
如果我们将联合类型插入ToArray
,则条件类型将应用于该联合的每个成员。
type ToArray<Type> = Type extends any? Type[] : never; type StrArrOrNumArr = ToArray<string | number>; type StrArrOrNumArr = string[] | number[]
这里发生的情况是ToArray
在以下联合上进行分布:
string | number;
并映射到每个联合成员类型,实际上是:
ToArray<string> | ToArray<number>;
这使我们得到:
string[] | number[];
通常,分布式是期望的行为。要避免这种行为,您可以在extends
关键字的每一侧用方括号括起来。
type ToArrayNonDist<Type> = [Type] extends [any]? Type[] : never; // 'ArrOfStrOrNum'不再是联合。 type ArrOfStrOrNum = ToArrayNonDist<string | number>; type ArrOfStrOrNum = (string | number)[]
结论
TypeScript 中的条件类型是强大的功能,能够实现高级类型操作,并有助于创建更精确和灵活的类型定义。它们在泛型编程和处理复杂类型转换时特别有用。