Preface
The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some new and revised chapters have been translated and sorted out.
This article is organized from the " Conditional Types " chapter in the TypeScript Handbook.
This article does not strictly follow the original translation, but also explains and supplements part of the content.
Conditional Types
In many cases, we need to determine the output value based on the input value, and we also need to determine the output value type based on the input value type. Conditional types (Conditional types ) is used to help us describe the relationship between input types and output types.
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
// type Example2 = string
The writing of conditional types is a bit similar to conditional expressions in JavaScript ( condition ? trueExpression : falseExpression
):
SomeType extends OtherType ? TrueType : FalseType;
From this example alone, you may not see the use of conditional types, but they are very useful when used with generics. Let us take the following createLabel
function as an example:
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
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 "unimplemented";
}
Function overloading is used here, describing how createLabel
makes different decisions based on different types of input values and returns different types. Pay attention to such things:
- If a library has to make the same choice after traversing the API over and over again, it becomes very cumbersome.
- We had to create three overloads, one to deal with clearly known types, we wrote an overload for each type (here one is
string
, one isnumber
), one is for the general case (receive onestring | number
). And if a new type is added, the number of overloads will increase exponentially.
In fact, we can completely write logic in conditional types:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
Using this condition type, we can simplify function overloading:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
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
Conditional Type Constraints
Usually, the use condition type will provide us with some new information. Just as using type guards (type guards) can narrowing the type (narrowing) provides us with a more specific type, the true
branch of the conditional type will further restrict the generic type, for example:
type MessageOf<T> = T["message"];
// Type '"message"' cannot be used to index type 'T'.
T
an error because 061d45c8b136a5 does not know that there is a property message
We can constrain T
so that TypeScript will no longer report an error:
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
However, if we want MessgeOf
be able to pass in any type, but when the passed-in value does not have the message
, then return the default type such as never
?
We can remove the constraints and use a condition type:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>;
// type DogMessageContents = never
In the true
branch, TypeScript will know that T
has a message
attribute.
To give another example, we write a Flatten
type to get the type of array elements. When the passed in is not an array, the passed in type is directly returned:
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten<string[]>;
// type Str = string
// Leaves the type alone.
type Num = Flatten<number>;
// type Num = number
Note that the index access type in the number
index is used here to obtain the type of the array element.
Inferring Within Conditional Types
The condition type provides the infer
keyword, which can be inferred from the type being compared, and then reference the inference result true
With the help of infer
, we modify Flatten
, and no longer use the index access type "manual" to obtain it:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Here we use the infer
keyword to declare a new type variable Item
, instead of explicitly writing out how to get T
true
branch, which can free us and let us no longer have to think about how to feel from us. Dig out the required type structure from the type structure of interest.
We can also use the infer
keyword to write some useful type helper aliases (helper type aliases) . For example, we can get the type returned by a function:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>;
// type Num = number
type Str = GetReturnType<(x: string) => string>;
// type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// type Bools = boolean[]
When the type is inferred from a multi-call signature (such as an overloaded function), it will be inferred according to the final signature, because this signature is generally used to handle all cases.
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
type T1 = ReturnType<typeof stringOrNum>;
// type T1 = string | number
Distributive Conditional Types
When using conditional types in generics, if you pass in a union type, it will become distributed (distributive) , for example:
type ToArray<Type> = Type extends any ? Type[] : never;
If we ToArray
, this conditional type will be applied to each member of the union type:
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]
Let us analyze StrArrOrNumArr
, this is the type we passed in:
string | number;
Next, iterate over the members in the union type, which is equivalent to:
ToArray<string> | ToArray<number>;
So the final result is:
string[] | number[];
Usually this is the desired behavior. If you want to avoid this behavior, you can wrap each part of the extends
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>;
// type StrArrOrNumArr = (string | number)[]
TypeScript series
The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.
WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.
If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。