Skip to main content

conditional-types

Conditional Types:

What Are Conditional Types:

Conditional types help you work with types that depend on other types. They are like "if" statements in your code, but for types. When TypeScript sees a conditional type, it decides which type to use based on the condition you've written.

// If 'T' is assignable to 'string', then 'Type' will be 'boolean', otherwise 'number'.
type IsString<T> = T extends string ? boolean : number;

Basic Syntax:

You write a conditional type with a special syntax that looks like T extends U ? X : Y. This means "if T can be assigned to U, then the type is X, otherwise it's Y".

// Here 'Result' will be 'true' because 'string' can be assigned to 'string | number'.
type Result = string extends string | number ? true : false;

Type Inference in Conditional Types:

Using the infer keyword inside conditional types allows TypeScript to infer types within the scope of the true branch of the conditional type. It's like asking TypeScript to figure out a specific type on its own.

// 'ReturnType' infers the return type of a function type 'T'.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

Distributive Conditional Types:

If you apply a conditional type to a union type, TypeScript will distribute the conditional type over each member of the union. It's as if you're asking, "Which of these options work with my condition?"

// 'NonNullable' removes 'null' and 'undefined' from the type 'T'.
type NonNullable<T> = T extends null | undefined ? never : T;

// Applied to a union, it will remove 'null' and 'undefined' from that union.
type StringOrNumber = NonNullable<string | number | null | undefined>;

Utility Types as Conditional Types:

Some utility types built into TypeScript, like Exclude and Extract, are actually conditional types. They're pre-made tools that use conditional types to help you manipulate other types.

// 'Exclude' will exclude certain types from a union.
type AvailableColors = 'red' | 'blue' | 'green';
type UsedColors = 'red' | 'green';
type UnusedColors = Exclude<AvailableColors, UsedColors>; // 'blue'

Nested Conditional Types:

You can nest conditional types within each other to create complex logic. It's like building a flowchart that TypeScript follows to find the right type.

// This nested conditional type checks if 'T' is an array, and if so,
// checks if its items are strings, returning appropriate types at each check.
type DeepType<T> = T extends Array<infer U>
? U extends string
? StringArray
: OtherArray
: T;

Conditional Types and IntelliSense:

When you use conditional types, code editors with IntelliSense, like Visual Studio Code, can provide smarter autocompletion suggestions because the editor understands the type conditions.

// There's no TypeScript code example for this point, as it's about the development environment experience rather than the code itself.

By leveraging conditional types, developers can write more robust and flexible type systems, guiding TypeScript to the correct types based on conditions in the codebase.Restricting Types with Conditional Types:

You can use conditional types to limit what types can be passed to a function or a component, which can prevent bugs by catching incorrect types early.

type NonZeroNumber<T extends number> = T extends 0 ? never : T;

// This function only accepts numbers that are not zero.
function processNonZeroNumber<T extends number>(value: NonZeroNumber<T>) {
// Implementation
}

Type Relationships and Conditional Types:

Conditional types can depend on type relationships. TypeScript checks if types are assignable, identical, or have an inheritance relationship, and then picks a type based on that relationship.

type IsString<T> = T extends string ? "Yes" : "No";

// Type is 'Yes' because 'myString' is assignable to 'string'.
type TestString = IsString<typeof myString>;

// Type is 'No' because 'myNumber' is not assignable to 'string'.
type TestNumber = IsString<typeof myNumber>;

Combining Conditional Types with Mapped Types:

You can use conditional types together with mapped types to transform objects' types in complex ways, depending on the properties of the objects.

type ReadOnlyOrWritable<T> = {
[P in keyof T]: T[P] extends Function ? T[P] : Readonly<T[P]>;
};

// This type will make all properties readonly except for functions.
type MyComplexObject = ReadOnlyOrWritable<{
id: number;
name: string;
update: (newName: string) => void;
}>;

Deferring Type Resolution:

Conditional types can defer the resolution of a type until TypeScript knows enough to decide on the correct type. This delay can help avoid errors when working with complex types.

type DeferredType<T> = T extends infer U ? U : never;

// The type resolution of 'Deferred' will be deferred until 'U' is known.
type Deferred = DeferredType<string>;

Conditional Types as Guards:

You can use conditional types as type guards to narrow down the type within a block of code, ensuring that the code block works with a specific type.

type StringOrNot<T> = T extends string ? T : never;

function processString<T>(value: StringOrNot<T>) {
if (typeof value === 'string') {
// TypeScript knows 'value' is a string here.
console.log(value.toUpperCase());
}
}

Avoiding Infinite Recursion in Conditional Types:

Be cautious of creating conditional types that reference themselves, leading to infinite recursion. TypeScript has limits to prevent this but designing conditional types carefully is important.

// Avoid:
type RecursiveType<T> = T extends any[] ? RecursiveType<T[number]> : T; // This might lead to recursion issues.

Conditional Types in Library Design:

When creating TypeScript libraries, you can use conditional types to offer users more precise type checking based on the arguments they pass to functions or components.

type ParamType<T> = T extends (param: infer P) => any ? P : T;

// Library function example
function useLibraryFunction<T extends (param: any) => any>(func: T): ParamType<T> {
// Implementation
}

Conditional Types and Type Errors:

Conditional types can help you catch potential type errors during development rather than at runtime. They act like a compile-time test for your types.

type CheckArrayType<T> = T extends Array<any> ? T : never;

// This will raise a compile-time error if 'nonArray' is not an array.
function processArray<T>(nonArray: CheckArrayType<T>) {
// Implementation
}

Using conditional types in these ways can greatly enhance the type safety and usability of TypeScript code, especially in complex applications and libraries.