Conditional Types: Extends, Infer, and Distributive Conditions

Table of Contents

  • Introduction
  • What Are Conditional Types in TypeScript?
  • Basic Syntax of Conditional Types
  • Understanding extends in Conditional Types
  • The infer Keyword in Conditional Types
  • Distributive Conditional Types
  • Practical Examples
    • Example 1: Using extends in Conditional Types
    • Example 2: Using infer for Type Inference
    • Example 3: Distributive Conditional Types
  • Best Practices for Using Conditional Types
  • Conclusion

Introduction

Conditional types are one of the most powerful features in TypeScript, enabling developers to define types that depend on certain conditions. They allow you to conditionally assign a type based on another type, offering greater flexibility in type definitions.

Conditional types are expressed with the following syntax:

T extends U ? X : Y

Where:

  • T is the type being checked.
  • U is the type being tested against.
  • X is the result if the condition T extends U is true.
  • Y is the result if the condition T extends U is false.

Conditional types open up possibilities for dynamic and context-sensitive type assignments, enabling a more powerful and expressive type system.

This article will dive into the key components of conditional types, such as extends, infer, and distributive conditional types. We’ll explore each concept with detailed examples and show you how to use them effectively.


What Are Conditional Types in TypeScript?

Conditional types allow you to create types that depend on a condition. They are particularly useful when you need to define types that vary based on the structure or properties of other types.

For example, let’s say you want to create a type that is different depending on whether the provided type is a string or a number. With conditional types, you can express this as:

type MyType<T> = T extends string ? "String type" : "Other type";

Here, MyType will resolve to "String type" if the type passed in T is string, and "Other type" otherwise.


Basic Syntax of Conditional Types

The basic syntax for conditional types in TypeScript is:

T extends U ? X : Y
  • T: The type that is checked against the condition.
  • U: The type that is being tested.
  • X: The resulting type if the condition is true (T extends U).
  • Y: The resulting type if the condition is false (T extends U).

Example:

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

type Test1 = IsString<string>; // "Yes"
type Test2 = IsString<number>; // "No"

In this example:

  • IsString<string> resolves to "Yes" because string extends string.
  • IsString<number> resolves to "No" because number does not extend string.

Understanding extends in Conditional Types

The extends keyword plays a pivotal role in conditional types. It is used to check whether a given type extends (or is compatible with) another type. If the type extends, the first part of the conditional type (X) is chosen; otherwise, the second part (Y) is used.

Example: Using extends in Conditional Types

type IsArray<T> = T extends any[] ? "Array" : "Not an Array";

type Result1 = IsArray<number[]>; // "Array"
type Result2 = IsArray<string>; // "Not an Array"

Here:

  • IsArray<number[]> resolves to "Array" because number[] extends any[].
  • IsArray<string> resolves to "Not an Array" because string does not extend any[].

This type of conditional logic is extremely helpful when you need to differentiate between various types and provide tailored behavior accordingly.


The infer Keyword in Conditional Types

The infer keyword is another powerful feature that TypeScript provides for working with conditional types. It allows you to infer a type within a conditional expression.

Example: Using infer for Type Inference

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

type Func1 = ReturnTypeOf<() => string>; // string
type Func2 = ReturnTypeOf<(a: number) => void>; // void
type Func3 = ReturnTypeOf<() => number[]>; // number[]

Here:

  • ReturnTypeOf checks if the type T extends a function type. If it does, it infers the return type (R) of the function. Otherwise, it returns never.
  • In the case of Func1, the return type of the function is inferred as string.
  • In Func2, the return type is inferred as void because the function doesn’t return anything.
  • Func3 returns a number[] since the function returns an array of numbers.

Why infer Is Powerful

The infer keyword enables TypeScript to dynamically capture types within a conditional block, making it extremely useful for type transformations or complex type inference.


Distributive Conditional Types

Distributive conditional types are a special case in TypeScript where conditional types distribute over union types. This means that if you pass a union type to a conditional type, TypeScript applies the condition to each member of the union.

Example: Distributive Conditional Types

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

type Test = IsString<string | number>; // "Yes" | "No"

Here:

  • IsString<string | number> is distributed over the union, meaning the type is evaluated for both string and number. This results in "Yes" | "No".

This behavior is highly useful when you need to create types that work flexibly with multiple possible types, as it allows TypeScript to apply conditions to each type individually.


Practical Examples

Example 1: Using extends in Conditional Types

type IsBoolean<T> = T extends boolean ? "Yes" : "No";

type Test1 = IsBoolean<boolean>; // "Yes"
type Test2 = IsBoolean<number>; // "No"

In this example:

  • IsBoolean<boolean> resolves to "Yes" because boolean extends boolean.
  • IsBoolean<number> resolves to "No" because number does not extend boolean.

Example 2: Using infer for Type Inference

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

type Result1 = FunctionReturnType<() => number>; // number
type Result2 = FunctionReturnType<(x: string) => boolean>; // boolean

Here:

  • FunctionReturnType uses infer to capture the return type of the function passed as T.

Example 3: Distributive Conditional Types

type Flatten<T> = T extends (infer U)[] ? U : T;

type Test1 = Flatten<number[]>; // number
type Test2 = Flatten<string[]>; // string
type Test3 = Flatten<boolean>; // boolean

In this example:

  • The type Flatten takes a type T and checks if it is an array. If it is, it returns the element type (U) of the array; otherwise, it returns T itself.
  • Flatten<number[]> resolves to number.
  • Flatten<boolean> resolves to boolean.

Best Practices for Using Conditional Types

  1. Use extends for Type Checks: When you need to test whether a type extends another, extends is the go-to approach. It allows you to create flexible conditional types that adapt to the structure of the types you work with.
  2. Leverage infer for Type Inference: infer is useful when you want to extract types from more complex structures like functions. Use it to capture return types, parameter types, and other dynamic types.
  3. Distributive Conditional Types: When working with union types, be aware that TypeScript applies the conditional logic to each member of the union. This can be used to create powerful, type-safe utilities that handle multiple types flexibly.
  4. Keep Types Simple: While conditional types offer powerful features, try to keep your type logic simple and understandable. Complex conditional types can make code harder to maintain.
  5. Avoid Overuse of never: Be cautious when using never as a fallback in conditional types. It can sometimes lead to overly restrictive types, so use it only when you expect that a condition should never be true.

Conclusion

Conditional types in TypeScript are an incredibly powerful feature that provides flexibility and type safety when working with types that depend on other types. By understanding and mastering the use of extends, infer, and distributive conditional types, you can create more dynamic, reusable, and robust code.

Through real-world examples and best practices, we’ve demonstrated how these features allow you to build complex type logic, making your TypeScript applications more precise and type-safe.

As you continue to explore TypeScript’s advanced types, conditional types will become an essential part of your toolkit, enabling you to write more flexible and intelligent code.