TypeScript Conditional Types
Conditional Types are one of the most powerful features in the TypeScript type system.
They allow types to be selected dynamically based on conditions, similar to ternary expressions in programming languages.
Conditional types make type definitions more flexible and are the foundation for implementing advanced utility types.
SVG Diagram: Conditional Type Flow
Why Do We Need Conditional Types?
In real TypeScript development, we often need to return different types based on different input types.
For example, a function may accept string or number parameters, and we need to return different result types based on the parameter type.
Conditional types provide the ability to perform logical judgment at the type level, making type definitions more flexible and powerful.
Concept Explanation:The syntax of conditional types is
T extends U ? X : YIf type T can be assigned to type U, return type X; otherwise, return type Y.
Basic Syntax
Conditional types use the ternary expression syntax to perform conditional checks at the type level.
This allows us to dynamically compute the return type based on the input type.
Example
// If T is a string type, return true; otherwise, return false
type IsString<T> = T extends string ? true : false;
// Using a conditional type
// string extends string is true, so type A is true
type A = IsString<string>;
// number extends string is false, so type B is false
type B = IsString<number>;
// Using these types
var a: A = true;
var b: B = false;
console.log("Is string a string?: " + a);
console.log("Is number a string?: " + b);
Output:
string 是字符串?: true number 是字符串?: false
Explanation:Conditional types are automatically evaluated during type checking to produce concrete types.
Practical Application: Type Filtering
One of the most common applications of conditional types is type filtering.
For example, you can create a type to exclude null and undefined.
Example
// If T is null or undefined, return never (empty type); otherwise, return T itself
type NonNullable<T> = T extends null | undefined ? never : T;
// Using the NonNullable type
// string is not null/undefined, so the type is string
type A = NonNullable<string>;
// null is null/undefined, so the type is never
type B = NonNullable<null>;
// undefined is null/undefined, so the type is never
type C = NonNullable<undefined>;
// Verifying the type
var a: A = "hello";
console.log("Non-null: " + a);
Output:
非空: hello
The never type:never represents a type that never exists. When a condition is not satisfied, TypeScript uses never to represent an "unavailable" type.
Type Inference: The infer Keyword
The infer keyword is one of the most powerful features in conditional types.
It allows "extracting" or "inferring" specific type parts from a type.
Example
// If T is a function type, return the inferred return type R; otherwise, return never
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Define a function that returns a user object
function getUser() {
return { name: "Alice" };
}
// Define a function that returns a number
function getNumber() {
return 42;
}
// Using ReturnType to get the function return type
// Inferred as { name: string }
type R1 = ReturnType<typeof getUser>;
// Inferred as number
type R2 = ReturnType<typeof getNumber>;
// Using the inferred types
var r1: R1 = { name: "Bob" };
var r2: R2 = 100;
console.log("User: " + JSON.stringify(r1));
console.log("Number: " + r2);
Output:
User: {"name":"Bob"}
数字: 100
The role of infer:infer is like a "variable" in the type system; it can capture a specific part of a type and use it in the resulting type.
Distributive Conditional Types
When the generic parameter of a conditional type is a union type, "distribution" is automatically applied.
That is, the condition is applied to each member of the union type separately, and the results are then merged.
Example
// When T is a union type, each type is automatically distributed
type ToArray<T> = T extends any ? T[] : never;
// Union types are automatically distributed
// string | number is distributed as: ToArray<string> | ToArray<number>
// i.e., string[] | number[]
type StrOrNum = ToArray<string | number>;
// It can be assigned string[] or number[]
var arr: StrOrNum = ["hello"];
// It can also be assigned number
arr = 42;
console.log("Array: " + arr);
Output:
数组: 42
Distribution mechanism:The distributive behavior of conditional types is enabled automatically. To disable distribution, you can use square brackets:
[T] extends U。
Combining Conditional Types with Mapped Types
Conditional types can be combined with mapped types to create powerful type transformation utilities.
This combination is the foundation for implementing TypeScript's built-in utility types.
Example
interface User {
// User ID
id: number;
// Username
name: string;
// User email
email: string;
}
// Implement Partial using mapped types and conditional types
// Iterate over all properties of T and add the optional modifier ?
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Implement Required using mapped types and conditional types
// Iterate over all properties of T and remove the optional modifier -?
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Using Partial: all properties become optional
var partial: Partial<User> = { name: "Alice" };
// Using Required: make the properties of Partial<User> required
// The Partial<User> type needs to exist first
type RequiredUser = Required<Partial<User>>;
var required: RequiredUser = { name: "Bob", id: 1 };
console.log("Optional: " + JSON.stringify(partial));
console.log("Required: " + JSON.stringify(required));
Output:
可选: {"name":"Alice"}
必填: {"name":"Bob","id":1}
Combined usage:The combination of conditional types and mapped types can implement various complex type transformations, such as built-in utility types like Partial, Required, and Readonly.
Advanced Example: Type Checking
Conditional types can also be used to implement more complex type checks.
Below are some useful type-checking examples in actual development.
Example
// Intersecting any with any type results in any, so 0 extends any is true
type IsAny<T> = 0 extends (1 & T) ? true : false;
// Testing IsAny
// any is a special type; intersecting it with any type returns any
type A = IsAny<any>; // true
// string is not any
type B = IsAny<string>; // false
console.log("Is any any?: " + A);
console.log("Is string any?: " + B);
// Check whether the type can be assigned
// Returns true if T can be assigned to U, otherwise returns false
type IsAssignableTo<T, U> = T extends U ? true : false;
// Test type assignability
// string can be assigned to any, so returns true
type CanAssign = IsAssignableTo<string, any>;
console.log("string assignable to any?: " + CanAssign);
Running result:
any 是 any?: true string 是 any?: true
Note:any is the most "lenient" type in TypeScript. It can be assigned to any type, and it can receive assignments from any type.
Notes
- Lazy evaluation:Conditional types are lazily evaluated; they are only computed when concrete types are used.
- Distributive property:Union types automatically trigger the distributive mechanism of conditional types.
- infer can only be used in extends conditions:This is the only place where infer can appear.
- Combining with mapped types:Combining conditional types with mapped types can create powerful utility types.
Advanced:Many TypeScript built-in utility types (such as Partial, Required, Extract) are implemented using conditional types.
Summary
Conditional types are one of the most powerful features in the TypeScript type system.
- Basic syntax:
T extends U ? X : Y - The infer keyword:Infer specific parts from a type.
- Distributive property:Union types are automatically distributed.
- Application scenarios:Type filtering, type inference, type checking.
- Utility types:Most built-in utility types are implemented based on conditional types.
Best practices:Mastering conditional types allows you to write more flexible and type-safe code.
Other extensions