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 Background Title How Conditional Types Work Syntax Explanation Conditional Type Syntax T extends U ? X : Y Arrow Execution Flow Execution Flow Check whether T can be assigned to U → Yes → X | No → Y Example Area Example Analysis Example 1 Example 1 IsString<string> string extends string? ✓ → true Example 2 Example 2 IsString<number> number extends string? ✗ → false Example 3 Example 3 ReturnType<fn> fn extends (...args) → R (inferred return type) Arrow Marker

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 isT 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

// Conditional type syntax: T extends U ? X : Y
// 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

// Implementing NonNullable with a conditional type
// 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

// Using infer to infer a function's return type
// 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

// ToArray converts type T to an array type
// 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

// Define a user interface
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

// Check whether the type is any
// 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