Conditional Types in TypeScript
Conditional Types
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
The last major tool for GENUINE type logic: conditional types – "if-then" decisions, but at the TYPE level instead of the value level, evaluated at compile time.
The basic syntax
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // falseT extends string ? true : false reads almost like the regular JavaScript ternary operator (condition ? then : else) – extends here does NOT check inheritance (as with classes, chapter 13), but rather "is T ASSIGNABLE to string?", evaluated at the TYPE level.
In practice: extracting a method's return type
Let's build a conditional type that automatically extracts the ELEMENT type from ANY Repository<T> class (chapters 17-18) – useful when you only know the type from the repository class, without writing it out a second time manually:
import { Repository } from '../repository/Repository.js';
export type ExtractElementType<R> = R extends Repository<infer T> ? T : never;
// Applied:
type BookRepo = Repository<import('../models/Book.js').Book>;
type ExtractedType = ExtractElementType<BookRepo>; // Bookinfer T is the KEY to conditional types: it tells TypeScript "figure OUT which concrete type sits at this position, and call it T" – infer can ONLY be used inside the extends clause of a conditional type, nowhere else.
Distributive conditional types over unions
type WithoutArray<T> = T extends any[] ? never : T;
type Mixed = string | number[] | boolean;
type Filtered = WithoutArray<Mixed>; // string | boolean - number[] got removed!A SUBTLE but important detail: when a conditional type gets applied to a UNION, it gets AUTOMATICALLY applied to EACH member of the union INDIVIDUALLY, and the results are merged back into a union ("distributive conditional types") – never from chapter 4/19 disappears from the resulting union in the process, since string | never simplifies to string.
Preventing distribution with square brackets
type WithoutArrayNonDistributed<T> = [T] extends [any[]] ? never : T;
type Result = WithoutArrayNonDistributed<Mixed>; // never - the ENTIRE union checked, not member by member[T] (T wrapped in square brackets) PREVENTS the automatic distribution – sometimes desired when you want to check the ENTIRE union as ONE type, instead of member by member. An advanced detail, rarely needed, but explains surprising behavior if it ever comes up.
Built-in examples: Awaited and NonNullable
type A = Awaited<Promise<string>>; // string - "unwraps" a promise
type B = NonNullable<string | null | undefined>; // string - removes null/undefinedAwaited and NonNullable from chapter 20 are THEMSELVES implemented as conditional types with infer – now you understand EXACTLY how they work internally, not just how to use them.
Tipp: Rule of thumb: writing conditional types YOURSELF is ADVANCED and rarely needed in everyday application code – usually an already built-in utility type (chapter 20) is enough. Most useful when writing REUSABLE library types meant to adapt to ANY input type – EXACTLY as we demonstrated here with ExtractElementType.