Suggest an editImprove this articleRefine the answer for “Conditional types in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Conditional types** in TypeScript evaluate `T extends U ? X : Y` at compile time, selecting one of two types based on assignability. When T is a bare generic, the condition distributes across each union member separately. ```typescript type Unwrap<T> = T extends Promise<infer U> ? U : T; type R = Unwrap<Promise<string>>; // string ``` **Key point:** distribution only happens with a naked generic parameter, not when T is wrapped.Shown above the full answer for quick recall.Answer (EN)Image**Conditional types** in TypeScript select one of two types based on whether `T` is assignable to `U`, using `T extends U ? X : Y` syntax that runs entirely at compile time. ## Theory ### TL;DR - Syntax: `T extends U ? X : Y`. T fits U, you get X. Otherwise you get Y. - Think vending machine: insert a type, the machine checks if it matches, dispenses one result. - Main difference from unions: conditional types distribute over each union member separately when T is a bare generic. - `infer` captures a sub-type during the check: `T extends Promise<infer U> ? U : T`. - Built on this: `ReturnType`, `Exclude`, `Extract`, `NonNullable`, `Parameters`. ### Quick example ```typescript type IsString<T> = T extends string ? "yes" : "no"; type A = IsString<string>; // "yes" type B = IsString<number>; // "no" type C = IsString<"hi">; // "yes" -- literal "hi" is a subtype of string ``` `T extends string` tests structural assignability, not identity. String literals pass because they are subtypes of `string`. ### Key difference from unions A union like `string | number` lists possibilities. A conditional type runs a check and produces one specific result. That is why `ReturnType<T>` works but no union can replicate it: you need to test `T extends (...args: any[]) => infer R` to extract R. Unions have no mechanism for that. ### When to use - Extract a function's return type or parameter types -> use `infer`. - Filter union members by removing matches -> `T extends SomeType ? never : T` (that is `Exclude` exactly). - Apply a transformation only to object types -> `T extends object ? DeepPartial<T> : T`. - Build a utility type that branches on shape instead of writing 3-4 manual overloads. If a plain union or intersection does the job, skip the conditional. Not every type problem needs a ternary. ### Distribution over unions When you pass a union to a conditional type with a bare generic T, TypeScript splits the union and runs the condition per member: ```typescript type Wrap<T> = T extends any ? [T] : never; type A = Wrap<string | number>; // Runs as: Wrap<string> | Wrap<number> // Result: [string] | [number] ``` If T is wrapped in brackets, an array, or another generic, distribution stops: ```typescript type WrapFixed<T> = [T] extends [any] ? T[] : never; type B = WrapFixed<string | number>; // Result: (string | number)[] -- treated as one type ``` Wrapping in `[T]` is the standard way to opt out of distribution. ### The `infer` keyword `infer` declares a type variable inside the condition and lets TypeScript capture whatever type fits that position: ```typescript type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type A = UnwrapPromise<Promise<string>>; // string type B = UnwrapPromise<number>; // number -- passes through unchanged ``` Stack `infer` to pull out tuple heads or function params: ```typescript type Head<T> = T extends [infer First, ...infer Rest] ? First : never; type H = Head<[string, number, boolean]>; // string ``` ### How the compiler resolves this TypeScript tests assignability structurally in a single-pass resolution. For distributed conditionals, it runs the condition once per union member when T is in a bare generic position. Zero runtime cost. The whole mechanism compiles away to plain JavaScript. One subtlety: if T is still unresolved inside a generic function body, the compiler keeps the conditional unevaluated until the call site provides a concrete type. ### Common mistakes **Expecting distribution when T is not bare** ```typescript // Distributes -- T is naked here type Id<T> = T extends any ? T : never; type X = Id<string | number>; // string | number // Does NOT distribute -- T is wrapped in Array<> type Wrapped<T> = Array<T> extends any ? T : never; type Y = Wrapped<string | number>; // string | number -- no split ``` Distribution only triggers when the type being tested is the naked generic T, not T wrapped in something else. **Using `any` in the condition widens literals** ```typescript // Bad -- "a" widens to string type ToArray<T> = T extends any ? T[] : never; type Leak = ToArray<"a" | "b">; // string[] // Good -- literals preserved type ToArraySafe<T> = T extends unknown ? T[] : never; type Fixed = ToArraySafe<"a" | "b">; // "a"[] | "b"[] ``` `unknown` preserves literal types. `any` triggers fresh inference and often widens them. **Forgetting that `never extends U` is always true** ```typescript type A = never extends string ? true : false; // true ``` `never` is a subtype of every type, so it passes any `extends` check. Union members that evaluate to `never` drop out of the result automatically. That is usually the intended behavior in filters like `Exclude`, but it surprises people the first time they see it. ### Real-world usage - React Query: `UseQueryResult<TData>` uses conditional inference to unwrap async data types. - Zod: `z.infer<typeof schema>` resolves through `T extends z.ZodType ? T['_output'] : never`. - tRPC: procedure types branch on `TRPCError` to build typed `Result<Success, Failure>` wrappers. - TypeScript stdlib: `Exclude`, `Extract`, `NonNullable`, `ReturnType`, `Parameters` are all conditional types under the hood. ### Follow-up questions **Q:** Write a conditional type that extracts all parameter types of a function. **A:** `type Params<T> = T extends (...args: infer P) => any ? P : never;` Here P captures the full parameter tuple, so `Params<(a: string, b: number) => void>` gives `[string, number]`. **Q:** What is the difference between `T extends U ? X : Y` and `[T] extends [U] ? X : Y`? **A:** The bracket version disables distribution. Without brackets, a union T splits and the condition runs per member. With brackets, T is treated as one type regardless of whether it is a union. **Q:** Why does `never extends string` evaluate to `true`? **A:** `never` is the bottom type, a subtype of everything. So it passes any `extends` check. When filtering a union, members that become `never` fall out of the result, which is exactly the behavior `Exclude` and `NonNullable` rely on. **Q:** How do conditional types interact with mapped types? **A:** You can combine them to filter object keys. `type Filter<T, U> = { [K in keyof T]: T[K] extends U ? T[K] : never }` keeps only values assignable to U. Append `[keyof T]` to collapse the object to a union of matching values. **Q:** (Senior) Why do both `T extends any` and `T extends unknown` distribute, but `[T] extends [any]` does not? **A:** Distribution is triggered by the position of bare T in the condition, not by what T is tested against. Wrapping T in `[T]` changes its position from bare generic to tuple member. That single structural change stops the distribution mechanism entirely. ## Examples ### Extracting return type with `infer` The pattern behind TypeScript's built-in `ReturnType<T>`: ```typescript type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never; async function fetchUser(): Promise<{ id: number; name: string }> { return { id: 1, name: "Alice" }; } type FetchResult = MyReturnType<typeof fetchUser>; // Promise<{ id: number; name: string }> // Combine with Awaited to unwrap the promise: type UserData = Awaited<FetchResult>; // { id: number; name: string } ``` `infer R` captures whatever the function returns. If T is not callable, the result is `never`. ### Filtering a union with distribution ```typescript type OnlyStrings<T> = T extends string ? T : never; type Mixed = "admin" | "user" | 42 | true; type StringRoles = OnlyStrings<Mixed>; // "admin" | "user" ``` TypeScript runs `OnlyStrings` on each union member separately. Number and boolean become `never` and drop out of the final union. This is exactly how `Extract<T, string>` works internally. ### Recursive DeepPartial ```typescript type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; interface Config { server: { host: string; port: number; }; debug: boolean; } type PartialConfig = DeepPartial<Config>; // { // server?: { host?: string; port?: number }; // debug?: boolean; // } ``` The condition `T extends object` splits objects from primitives. Primitives pass through unchanged; objects get recursively wrapped. I have used this pattern when building API clients where patch endpoints accept updates at any nesting depth, without defining a separate partial type for each nested interface.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.