TypeScript 条件类型如何工作

TypeScript 中的条件类型使我们能够根据逻辑定义某些类型,就像我们在代码的其他方面所做的那样。它们是在 TypeScript中定义类型的有用工具。

它们采用一种熟悉的格式,因为我们像这样编写它们condition ? ifConditionTrue : ifConditionFalse– 这种格式已经在 TypeScript 和 Javascript 中随处使用。让我们看看它们是如何工作的。

条件类型在 TypeScript 中的工作原理#

让我们看一个简单的例子来理解它是如何工作的。这里,一个值可以是用户的出生日期 (DOB) 或年龄。如果是出生日期,那么类型应该是字符串 – 但如果是年龄,则应该是数字。我们将定义三种类型:DobAgeUserAgeInformation

type Dob = string;
type Age = number;
type UserAgeInformation<T> = T extends number ? number : string;

如前所述,Dob将是一个string,像12/12/1942,并且Age,应该是一个number,像96

当我们定义 时UserAgeInformation,我们是这样写的:

type UserAgeInformation<T> = T extends number ? number : string;

T的论点在哪里UserAgeInformation。我们可以在这里传递任何类型。然后我们说,如果T extends number,那么类型是number。否则就是string. 我们在这里本质上说的是,如果T是 type number,那么UserAgeInformation应该是 a number

如果我们希望它是一个数字,我们可以传入Agein ,如果我们希望它是一个字符串:userAgeInformationDob

type Dob = string;
type Age = number;
type UserAgeInformation<T> = T extends number ? number : string;

let userAge:UserAgeInformation<Age> = 100;
let userDob:UserAgeInformation<Dob> = '12/12/1945';

将条件类型与 keyof 组合#

T我们可以通过检查是否扩展了一个对象来更进一步。例如,假设我们经营的企业有两种类型的客户:Horses 和Users。虽然 aUser有一个地址,但 aHorse通常只有一个位置。对于每一个,我们都有不同的地址格式,如下所示:

type User = {
    age: number,
    name: string,
    address: string
}

type Horse = {
    age: number,
    name: string
}

type UserAddress = {
    addressLine1: string,
    city: string,
    country: string,
}

type HorseAddress = {
    location: 'farm' | 'savanna' | 'field' | 'other'
}

未来我们可能还会有其他类型的客户,所以我们可以通用检查是否T有属性address。如果是,请使用UserAddress. 否则,使用 theHorseAddress作为最终类型:

type AddressComponents<T> = T extends { address: string } ? UserAddress : HorseAddress

let userAddress:AddressComponents<User> = {
    addressLine1: "123 Fake Street",
    city: "Boston",
    country: "USA"
}

let horseAddress:AddressComponents<Horse> = {
    location: 'farm'
}

当我们说 时T extends { address: string },我们检查它是否T有属性address。如果是这样,我们将使用UserAddress. 否则,我们可以默认为HorseAddress.

在条件返回中使用 T#

我们甚至可以T在条件返回中使用它自己。在这个例子中,sinceT被定义为User当我们调用它 ( UserType<User>) 时,myUser它的类型是User,并且需要在该类型 ( agenameaddress) 中定义的字段:

type User = {
    age: number,
    name: string,
    address: string
}

type Horse = {
    age: number,
    name: string
}

type UserType<T> = T extends { address: string } ? T : Horse

let myUser:UserType<User> = {
    age: 104, 
    name: "John Doe",
    address: "123 Fake Street"
}

在类型输出中使用 T 时的联合类型

如果我们在这里传递一个联合类型,每个都将单独测试。例如,假设我们做了以下事情:

type UserType<T> = T extends { address: string } ? T : string
let myUser:UserType<User | Horse> = {
    age: 104, 
    name: "John Doe",
    address: "123 Fake Street"
}

myUser,上面,实际上变成了类型User | string。那是因为虽然User通过了条件检查,Horse但没有通过 – 所以它返回字符串。

如果我们以某种方式修改 T(比如将其设为数组)。所有T值都将单独修改。例如,看下面的例子:

type User = {
    age?: number,
    name: string,
    address?: string
}
type Horse = {
    age?: number,
    name: string
}
type UserType<T> = T extends { name: string } ? T[] : never;
//   ^ -- will return the type arguement T as T[], if T contains the property `name` of type `string`
let myUser:UserType<User | Horse> = [{ name: "John" }, { name: "Horse" }]
//  ^ -- becomes User[] | Horse[], since both User and Horse have the property name

在这里,我们已经简化User并且Horse只有所需的属性name。在我们的条件类型中,两种类型都包含属性name。因此,两者都返回 true,并且返回的类型是T[]. 由于两者都返回 true,myUser类型为User[] | Horse[],所以我们可以简单地提供一个包含 name 属性的对象数组。

这种行为通常很好,但在某些情况下User,您可能希望返回一个数组。Horse在这种情况下,如果我们想避免像这样分布类型,我们可以在Tand周围添加括号{ name: string }

type User = {
    age?: number,
    name: string,
    address?: string
}
type Horse = {
    age?: number,
    name: string
}
type UserType<T> = [T] extends [{ name: string }] ? T[] : never;
//   ^ -- here, we avoid distributing the types, since T and { name: string } are in brackets
let myUser:UserType<User | Horse> = [{ name: "John" }, { name: "Horse" }]
//  ^ -- that means the type is slightly different now - it is (User | Horse)[]

通过使用方括号,我们的类型现在已转换为(User | Horse)[],而不是User[] | Horse[]。这在某些特定情况下可能很有用,并且是关于条件类型的复杂性,需要记住。

使用条件类型推断类型#

我们也可以infer在使用条件类型时使用关键字。假设我们有两种类型,一种用于数字数组,另一种用于字符串数组。在这个简单的例子中,infer将推断数组中每个项目的类型,并返回正确的类型:

type StringArray = string[];
type NumberArray = number[];
type MixedArray = number[] | string[];
type ArrayType<T> = T extends Array<infer Item> ? Item : never;
let myItem1:ArrayType<NumberArray> = 45
//  ^ -- since the items in `NumberArray` are of type `number`, the type of `myItem` is `number`.
let myItem2:ArrayType<StringArray> = 'string'
//  ^ -- since the items in `StringArray` are of type `string`, the type of `myItem` is `string`.
let myItem3:ArrayType<MixedArray> = 'string'
//  ^ -- since the items in `MixedArray` can be `string` or `number, the type of `myItem is `string | number`

在这里,我们在条件类型中定义了一个新参数,称为Item,它是扩展中的Array项目T。值得注意的是,这仅在我们传入的类型是数组时才有效,因为我们使用的是Array<infer Item>.

如果 whereT是一个数组,则ArrayType返回其项的类型。如果T不是数组,那么ArrayType将是never.

结论#

TypeScript 中的条件类型一开始可能看起来令人困惑,但它基本上只是在某些特定情况下简化我们编写类型的另一种方式。如果您曾经在某个存储库或项目中看到它,或者对于简化您自己的代码库,了解它是如何工作的很有用。

我希望你喜欢这个指南。如果您这样做了,您可能还会喜欢我写的关于Record 实用程序类型的文章。