TypeScript 中的条件类型

65 篇文章 0 订阅

简介

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满足SomeConditioninfer 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 中的条件类型是强大的功能,能够实现高级类型操作,并有助于创建更精确和灵活的类型定义。它们在泛型编程和处理复杂类型转换时特别有用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

幻想多巴胺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值