Skip to main content

TypeScript Conditional Types

Introduction

TypeScript conditional types are one of the most powerful features in TypeScript's type system, allowing you to create types that change based on conditions. They enable you to create dynamic types that respond to the properties of other types—similar to how you might use an if statement in your regular code, but at the type level.

Conditional types follow this pattern:

typescript
type Result = SomeType extends OtherType ? TrueType : FalseType;

This might seem abstract at first, but once understood, conditional types become an invaluable tool for creating flexible, reusable type definitions.

Understanding the Basics

The extends Keyword

At the heart of conditional types is the extends keyword, which acts as a condition check. The syntax resembles a ternary operator in JavaScript:

typescript
type CheckNumber<T> = T extends number ? 'Yes, it is a number' : 'No, it is not a number';

// Usage
type IsItANumber = CheckNumber<42>; // 'Yes, it is a number'
type IsItANumberString = CheckNumber<'42'>; // 'No, it is not a number'

In this example:

  • CheckNumber<T> is a generic type that takes a type parameter T
  • It checks whether T extends number (is T a subtype of number?)
  • If true, the type becomes 'Yes, it is a number'
  • If false, the type becomes 'No, it is not a number'

Conditional Types with Unions

When a conditional type is applied to a union type, it distributes over each member of the union:

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

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

This is known as the distributive property of conditional types. TypeScript applies the condition to each member of the union separately.

Advanced Conditional Types

Extracting Types with infer

The infer keyword takes conditional types to another level by allowing you to extract type information from the condition:

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

// Usage
function greeting(): string {
return "Hello world!";
}

type GreetingReturnType = GetReturnType<typeof greeting>; // string

Here, infer R captures the return type of the function as a new type variable R, which is then used as the true branch of the conditional.

Predefined Conditional Types

TypeScript provides several built-in conditional types:

  1. Extract<T, U> - Extracts all members of T that are assignable to U
  2. Exclude<T, U> - Excludes all members of T that are assignable to U
  3. NonNullable<T> - Removes null and undefined from T
  4. ReturnType<T> - Gets the return type of a function type
  5. Parameters<T> - Gets the parameter types of a function type as a tuple
typescript
// Extract
type OnlyNumbers = Extract<string | number | boolean, number>; // number

// Exclude
type NoNumbers = Exclude<string | number | boolean, number>; // string | boolean

// NonNullable
type NotNull = NonNullable<string | null | undefined>; // string

// ReturnType
type FuncReturn = ReturnType<() => number>; // number

// Parameters
type FuncParams = Parameters<(a: string, b: number) => void>; // [string, number]

Real-World Applications

Type-Safe Event Handlers

Conditional types help create type-safe event handlers based on event types:

typescript
type EventConfig = {
click: {
x: number;
y: number;
};
focus: {
id: string;
};
keydown: {
key: string;
shiftKey: boolean;
};
};

type EventHandler<E extends keyof EventConfig> = (data: EventConfig[E]) => void;

// Usage
const handleClick: EventHandler<'click'> = (data) => {
console.log(`Clicked at ${data.x}, ${data.y}`);
};

const handleKeydown: EventHandler<'keydown'> = (data) => {
console.log(`Key pressed: ${data.key}, shift: ${data.shiftKey}`);
};

Building a Type-Safe API Client

Conditional types can help build type-safe API clients:

typescript
interface ApiEndpoints {
'/users': {
get: {
response: { id: number; name: string }[];
};
post: {
body: { name: string; email: string };
response: { id: number; success: boolean };
};
};
'/posts': {
get: {
response: { id: number; title: string; content: string }[];
};
};
}

type ApiMethod = 'get' | 'post' | 'put' | 'delete';

type EndpointHasMethod<
Path extends keyof ApiEndpoints,
Method extends ApiMethod
> = Method extends keyof ApiEndpoints[Path] ? true : false;

type RequestBody<
Path extends keyof ApiEndpoints,
Method extends ApiMethod
> = EndpointHasMethod<Path, Method> extends true
? 'body' extends keyof ApiEndpoints[Path][Method]
? ApiEndpoints[Path][Method]['body']
: never
: never;

type ResponseData<
Path extends keyof ApiEndpoints,
Method extends ApiMethod
> = EndpointHasMethod<Path, Method> extends true
? 'response' extends keyof ApiEndpoints[Path][Method]
? ApiEndpoints[Path][Method]['response']
: never
: never;

// Simplified API client
function apiRequest<
Path extends keyof ApiEndpoints,
Method extends ApiMethod
>(
path: Path,
method: Method,
body?: RequestBody<Path, Method>
): Promise<ResponseData<Path, Method>> {
// Implementation would go here
return fetch(path, {
method,
body: body ? JSON.stringify(body) : undefined
}).then(res => res.json());
}

// Usage
async function example() {
// Correctly typed - body and response are properly inferred
const users = await apiRequest('/users', 'get');
// users is inferred as { id: number; name: string }[]

const newUser = await apiRequest('/users', 'post', {
name: 'John Doe',
email: '[email protected]'
});
// newUser is inferred as { id: number; success: boolean }
}

Filtering Object Properties by Type

Another practical use is filtering object properties based on their type:

typescript
type FilterByValueType<Obj, ValueType> = {
[Key in keyof Obj as Obj[Key] extends ValueType ? Key : never]: Obj[Key];
};

interface User {
id: number;
name: string;
isActive: boolean;
createdAt: Date;
tags: string[];
settings: { theme: string; notifications: boolean };
}

// Extract only string properties
type StringProperties = FilterByValueType<User, string>;
// { name: string }

// Extract only boolean properties
type BooleanProperties = FilterByValueType<User, boolean>;
// { isActive: boolean }

// Extract only object properties
type ObjectProperties = FilterByValueType<User, object>;
// { createdAt: Date; tags: string[]; settings: { theme: string; notifications: boolean } }

Understanding Mapped Types with Conditional Types

Conditional types become even more powerful when combined with mapped types:

typescript
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
id: string;
name: string;
price: number;
stock: number;
description: string;
}

// Make price and stock optional
type UpdateProduct = Optional<Product, 'price' | 'stock'>;

// Usage
const updateProduct: UpdateProduct = {
id: '1',
name: 'Laptop',
description: 'A powerful laptop'
// price and stock are now optional
};

Summary

TypeScript conditional types are a powerful mechanism that allows you to create dynamic and flexible type definitions. They work like type-level if-statements, enabling you to:

  • Create types that respond to the properties of other types
  • Extract specific types using the infer keyword
  • Distribute over union types
  • Build complex type utilities

By understanding and leveraging conditional types, you can create more type-safe, reusable code that can adapt to different situations and provide better developer experience.

Exercises

To reinforce your understanding of conditional types, try these exercises:

  1. Create a FunctionPropertyNames<T> type that extracts all property names of an object that are functions.
  2. Implement a DeepReadonly<T> type that makes all properties of an object (and nested objects) readonly.
  3. Create a conditional type IsEmpty<T> that checks if a type is {} or unknown or any.
  4. Build a UnionToIntersection<U> type that converts a union type to an intersection type.

Additional Resources

By mastering conditional types, you'll unlock powerful type manipulation capabilities and be able to create more flexible, reusable, and type-safe code.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)