TYPESCRIPT

TypeScript Conditional Types: Syntax, Behavior, and Practical Examples

Conditional types in TypeScript allow you to write logic directly within the type system. They help you define types that adapt based on relationships between other types, enabling more reusable, dynamic, and expressive code structures. These are especially useful when building APIs, utility types, UI components, or data transformation logic that must work across a variety of type inputs.

You’ll find conditional types heavily used in TypeScript’s built-in utility types, but they’re also powerful tools for your own projects.


What Are Conditional Types?

Conditional types follow a format similar to ternary logic in regular code:

T extends U ? X : Y

This means: if type T is assignable to type U, then use type X; otherwise, use type Y.

Example:

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

type A = IsString<string>;  // "Yes"
type B = IsString<number>;  // "No"

This basic check forms the foundation for branching behavior inside types.


Using Conditional Logic with Generics

You can combine generics with conditionals to dynamically control output types:

type ApiResponse<T> = T extends string ? string : number;

let res1: ApiResponse<string>;  // string
let res2: ApiResponse<boolean>; // number

This technique is helpful in building functions or types that adapt to input shape.


Inspecting Type Relationships

Conditional types can be used to check relationships between types—for example, to see if a value is a function:

type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type A = IsFunction<() => void>; // true
type B = IsFunction<string>;     // false

This helps with type guards, overloads, or advanced component props.


Extracting Types with infer

The infer keyword allows you to extract subtypes from a larger structure:

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

type A = ElementType<number[]>; // number
type B = ElementType<string>;   // string

You can use this to access array elements, return types, or other internal pieces of a type.


Conditional Return Types

You can extract the return type of a function dynamically:

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

type A = ReturnTypeOf<() => number>; // number
type B = ReturnTypeOf<string>;       // never

This is especially useful in generic helper functions or API abstractions.


Filtering Unions with Distributive Behavior

Conditional types naturally distribute over union types. You can use this to filter out or transform individual members:

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

type A = NotString<string | number | boolean>; // number | boolean

This lets you write custom utilities similar to Exclude or Extract.


Custom Utility Types

You can mimic some of TypeScript’s core utility types using conditionals.

Exclude-like

type MyExclude<T, U> = T extends U ? never : T;

type A = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"

Extract-like

type MyExtract<T, U> = T extends U ? T : never;

type B = MyExtract<"a" | "b" | "c", "a" | "c">; // "a" | "c"

These patterns give you precise control over which types to keep or remove from unions.


Nesting Conditional Logic

You can build complex decision trees with nested conditionals:

type Label<T> =
  T extends string ? "String" :
  T extends number ? "Number" :
  T extends boolean ? "Boolean" :
  "Other";

type A = Label<123>;   // "Number"
type B = Label<false>; // "Boolean"
type C = Label<null>;  // "Other"

This works well for mapping multiple types to specific labels or configurations.


Checking Object Structure

Use conditionals to check for the presence or structure of object properties:

type HasId<T> = T extends { id: number } ? true : false;

type A = HasId<{ id: number }>;   // true
type B = HasId<{ name: string }>; // false

You can use this to build validation logic or enforce structure at compile time.


Conditional Props in React

Conditional types can also shape React component props:

type Props<T> = T extends "input" ? { value: string } : { children: React.ReactNode };

function Component<T extends "input" | "div">(props: Props<T>) {
  // ...
}

This pattern supports flexible, polymorphic components without duplicating types.


Restricting Types with Constraints

Prevent certain types from being used by checking their structure:

type ValidKey<T> = T extends string | number | symbol ? T : never;

type A = ValidKey<"name">; // "name"
type B = ValidKey<true>;   // never

This pattern can restrict generic parameters or enforce safety in dynamic keys.


Optional Fields Based on Logic

You can apply conditional logic to control optionality:

type OptionalIf<T, U> = T extends U ? T | undefined : T;

type A = OptionalIf<"admin", "admin">; // "admin" | undefined
type B = OptionalIf<"user", "admin">;  // "user"

This is helpful when designing types for forms, role-based access, or feature flags.


Conditional Mapped Types

Conditional types combine well with mapped types for fine-grained control over object transformations:

type ConditionallyReadonly<T> = {
  [K in keyof T]: T[K] extends string ? Readonly<T[K]> : T[K];
};

type Data = {
  name: string;
  age: number;
};

type Result = ConditionallyReadonly<Data>; // 'name' is readonly

You can use this to model access rules, read-only fields, or conditional permissions.


Limitations and Gotchas

While conditional types are powerful, they do have caveats:

  • They automatically distribute over union types unless wrapped in square brackets.
  • Deep nesting can hurt readability—keep logic modular.
  • Complex types may be difficult for teammates to understand and maintain.

Keeping your conditional types simple and isolated helps avoid confusion.


Summary

Conditional types in TypeScript give you powerful tools to write logic-driven type definitions. They adapt based on type relationships, letting you filter, extract, or reshape types dynamically. Features like infer, distributive behavior, and nested conditions make them a core part of advanced TypeScript workflows.

Use conditional types to enhance type safety, reduce duplication, and model behavior more accurately—especially when building reusable components, APIs, or libraries. With care, they can make your codebase more expressive and robust.

Learn TypeScript for Free
Start learning now
button icon
To advance beyond this tutorial and learn TypeScript by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH

Reach your coding goals faster