TypeScript 中的条件类型使我们能够根据逻辑定义某些类型,就像我们在代码的其他方面所做的那样。它们是在 TypeScript中定义类型的有用工具。
它们采用一种熟悉的格式,因为我们像这样编写它们condition ? ifConditionTrue : ifConditionFalse
– 这种格式已经在 TypeScript 和 Javascript 中随处使用。让我们看看它们是如何工作的。
条件类型在 TypeScript 中的工作原理#
让我们看一个简单的例子来理解它是如何工作的。这里,一个值可以是用户的出生日期 (DOB) 或年龄。如果是出生日期,那么类型应该是字符串 – 但如果是年龄,则应该是数字。我们将定义三种类型:Dob
、Age
和UserAgeInformation
。
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
。
如果我们希望它是一个数字,我们可以传入Age
in ,如果我们希望它是一个字符串:userAgeInformation
Dob
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
我们可以通过检查是否扩展了一个对象来更进一步。例如,假设我们经营的企业有两种类型的客户:Horse
s 和User
s。虽然 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
,并且需要在该类型 ( age
, name
, address
) 中定义的字段:
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
在这种情况下,如果我们想避免像这样分布类型,我们可以在T
and周围添加括号{ 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 实用程序类型的文章。