阿里云主机折上折
  • 微信号
Current Site:Index > Application of conditional types

Application of conditional types

Author:Chuan Chen 阅读数:39300人阅读 分类: TypeScript

Basic Concepts of Conditional Types

Conditional types in TypeScript allow us to select different types based on the relationship between input types. They take the form T extends U ? X : Y, similar to the ternary operator in JavaScript. When type T is assignable to type U, the resulting type is X; otherwise, it is Y.

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>;  // true
type B = IsString<42>;       // false

Conditional types are particularly useful in generics, where they can dynamically determine the resulting type based on type parameters. They are often combined with the infer keyword to extract partial types from complex types.

Distributive Conditional Types

When conditional types operate on union types, they exhibit "distributive" behavior. This means the conditional type is distributed over each member of the union.

type ToArray<T> = T extends any ? T[] : never;

type StrArrOrNumArr = ToArray<string | number>;
// Equivalent to string[] | number[]

This feature makes it convenient to handle union types. To prevent distributive behavior, you can wrap both sides of extends in square brackets:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Arr = ToArrayNonDist<string | number>;
// Equivalent to (string | number)[]

Type Inference and the infer Keyword

The infer keyword allows us to declare a type variable within a conditional type to capture the type to be inferred. This is particularly useful for extracting parts of complex types.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

function foo() { return 42; }
type FooReturn = ReturnType<typeof foo>;  // number

infer can be used in various type positions, including function parameters, array elements, Promise resolution values, etc.:

type UnpackPromise<T> = T extends Promise<infer U> ? U : T;

type Num = UnpackPromise<Promise<number>>;  // number

Built-in Utility Conditional Types

TypeScript provides several built-in conditional type utilities, which are themselves implemented using conditional types:

  1. Exclude<T, U> - Excludes from T those types that are assignable to U.
type T = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
  1. Extract<T, U> - Extracts from T those types that are assignable to U.
type T = Extract<"a" | "b" | 1 | 2, string>;  // "a" | "b"
  1. NonNullable<T> - Excludes null and undefined from T.
type T = NonNullable<string | null | undefined>;  // string

Recursive Conditional Types

Conditional types can reference themselves recursively, enabling the handling of nested structures or recursive type transformations:

type DeepReadonly<T> = 
    T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> } : T;

type Nested = {
    a: number;
    b: {
        c: boolean;
        d: {
            e: string;
        };
    };
};

type ReadonlyNested = DeepReadonly<Nested>;

Recursive conditional types are powerful for deeply nested data structures but require attention to recursion depth limits.

Combining Conditional Types with Mapped Types

Conditional types can be combined with mapped types to create more flexible type transformations:

type FunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];

type Person = {
    name: string;
    age: number;
    greet: () => void;
};

type PersonMethods = FunctionPropertyNames<Person>;  // "greet"

This combination can filter properties of specific types or apply different transformations to properties of different types.

Type Constraints in Conditional Types

In conditional types, we can add additional constraints to type parameters:

type Flatten<T> = T extends Array<infer U> ? U : T;

type StringArray = Array<string>;
type StringItem = Flatten<StringArray>;  // string

This pattern is particularly useful when dealing with types that may or may not be arrays.

Conditional Types and Template Literal Types

Template literal types introduced in TypeScript 4.1 can be combined with conditional types:

type GetterName<T extends string> = `get${Capitalize<T>}`;

type NameGetter = GetterName<'name'>;  // "getName"

type PropType<T, K extends string> = 
    K extends `on${infer Event}` ? (e: Event) => void : T[K];

type Props = {
    onClick: (e: MouseEvent) => void;
    value: string;
};

type ClickHandler = PropType<Props, 'onClick'>;  // (e: MouseEvent) => void

This combination is particularly useful for handling event handlers or APIs with specific naming conventions.

Applications of Conditional Types in Complex Type Operations

Conditional types can be used to build complex type operations, such as type-safe Redux reducers:

type Action<T extends string = string, P = any> = {
    type: T;
    payload: P;
};

type ActionCreator<A extends Action> = (
    payload: A['payload']
) => A;

type Reducer<S = any, A extends Action = Action> = (
    state: S,
    action: A
) => S;

type Handlers<S, A extends Action> = {
    [T in A['type']]: Reducer<S, Extract<A, { type: T }>>
};

function createReducer<S, A extends Action>(
    initialState: S,
    handlers: Handlers<S, A>
): Reducer<S, A> {
    return (state = initialState, action) => {
        const handler = handlers[action.type];
        return handler ? handler(state, action) : state;
    };
}

This example demonstrates how to use conditional types to create type-safe Redux reducers, ensuring that action types match their corresponding handlers.

Conditional Types and Function Overloads

Conditional types can simplify the type definitions of function overloads:

type Overloads = {
    (x: string): number;
    (x: number): string;
};

type ReturnTypeByParam<T> = 
    T extends string ? number :
    T extends number ? string :
    never;

function overloaded<T extends string | number>(x: T): ReturnTypeByParam<T>;
function overloaded(x: any): any {
    return typeof x === 'string' ? x.length : x.toString();
}

const a = overloaded('hello');  // number
const b = overloaded(42);      // string

This approach is more concise than traditional function overloads, especially when there are many overloads.

Conditional Types and Recursive Type Limits

TypeScript imposes limits on recursive type depth, which may require special techniques when dealing with deep recursion:

type MaxDepth = 5;  // Prevent infinite recursion

type DeepPartial<T, Depth extends number = 0> = 
    Depth extends MaxDepth ? T :
    T extends object ? {
        [P in keyof T]?: DeepPartial<T[P], AddOne<Depth>>;
    } : T;

type AddOne<N extends number> = 
    [1, 2, 3, 4, 5, 6][N];  // Simple number increment

interface ComplexObject {
    a: {
        b: {
            c: {
                d: {
                    e: string;
                };
            };
        };
    };
}

type PartialComplex = DeepPartial<ComplexObject>;

This example shows how to limit recursion depth to prevent the type checker from entering infinite recursion.

Conditional Types and Type Predicates

Conditional types can be combined with type predicates to create more precise type guards:

type IsArray<T> = T extends any[] ? true : false;

function filterArray<T>(items: T[] | T): IsArray<T> extends true ? T[] : T {
    return Array.isArray(items) ? items : [items] as any;
}

const arr = filterArray([1, 2, 3]);    // number[]
const single = filterArray(42);        // number

Although type assertions are needed, this pattern can provide better type safety in complex scenarios.

Conditional Types and Variadic Tuple Types

Variadic tuple types introduced in TypeScript 4.0 can be combined with conditional types:

type Shift<T extends any[]> = 
    T extends [infer First, ...infer Rest] ? Rest : [];

type Tuple = [string, number, boolean];
type Rest = Shift<Tuple>;  // [number, boolean]

type Last<T extends any[]> = 
    T extends [...infer _, infer Last] ? Last : never;

type LastItem = Last<Tuple>;  // boolean

These utility types are particularly useful when working with tuples, especially in the context of function parameter lists.

Conditional Types and Branded Types

Conditional types can be used to create and handle branded types:

type Brand<T, B> = T & { __brand: B };

type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;

type IsBrand<T, B> = T extends Brand<infer U, B> ? true : false;

type A = IsBrand<UserId, 'UserId'>;  // true
type B = IsBrand<PostId, 'UserId'>; // false

Branded types help distinguish between semantically different but structurally identical types, while conditional types can inspect and handle these branded types.

Conditional Types and Type-Level Programming

Conditional types enable complex type-level programming, such as type arithmetic:

type Length<T extends any[]> = 
    T extends { length: infer L } ? L : never;

type Tuple = [string, number, boolean];
type Len = Length<Tuple>;  // 3

type BuildTuple<L extends number, T extends any[] = []> = 
    T['length'] extends L ? T : BuildTuple<L, [...T, any]>;

type Tuple3 = BuildTuple<3>;  // [any, any, any]

Although TypeScript's type system is not Turing-complete, conditional types allow us to implement many useful type-level computations.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.