Topic 9.1
Conditional Types
In one line
A conditional type, written T extends U ? X : Y, asks whether a type fits another type and picks one of two answers, so a type can change depending on what you give it.
Think of it like this
A post office sorting machine. A parcel comes in, the machine asks one question ("is it lighter than 1 kg?"), and sends it down the left belt or the right belt. A conditional type is a sorting machine for types. You put a type in, it asks "does this type fit that shape?", and it hands back one type for yes and another for no. Nothing is run; the compiler sorts types while it checks your code.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Conditional type
- A type that picks one of two types depending on a test, written
T extends U ? X : Y. - extends (in a condition)
- "Is assignable to": can a value of the left type be used where the right type is expected.
- Generic type alias
- A named type with a blank (a type parameter) that callers fill in, like
type Box<T> = { value: T }. - Type parameter
- The blank in a generic, usually written
T. The caller replaces it with a real type. - Branch
- One of the two answers of a conditional type: the
Xafter?or theYafter:. - Deferred
- Put on hold. While the compiler does not yet know
T, it cannot choose a branch, so it keeps the whole conditional type as it is. - Type-level
- Happening in the types only, while the compiler checks. It leaves no trace in the JavaScript that runs.
- Type assertion (`as`)
- Also called a cast (Topic 5.6). You tell the compiler "trust me, this value has this type". It checks nothing at runtime.
Step by step
01Ask a question about a type
Start with the smallest useful conditional type. It asks "is T a string?" and answers with the type true or the type false.
Types can be values too: true and false are literal types (Topic 4.2) that contain exactly one value each. That is why a type can "answer" with them.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false02Fill in the blank by hand
The compiler replaces T with what you wrote and tests the question. For IsString<42> it asks "does 42 fit in string?" No, so the result is the false branch.
Hover over A and B in the first live example to see true and false. You can try this with any type, including string itself: a value of type string fits in string, so the answer is true.
03Return something more useful than true or false
Answers can be any type. Flatten<T> says: "if T is an array, give me the type of its items, otherwise give me T itself."
T[number] means "the type you get when you index the array type with a number", which is the type of one item. You met this in Topic 8.3 (indexed access types).
type Flatten<T> = T extends unknown[] ? T[number] : T;
type Nums = Flatten<number[]>; // number
type Plain = Flatten<boolean>; // boolean04Chain the questions
Put a second conditional type in the "no" branch to get an if / else if / else. The compiler tries the questions from the top and stops at the first yes.
Always end with a plain fallback. Without it you would have nothing to answer when no question matches.
05Use a condition to shape objects
The branches can be whole object types. Result<T> gives a failure shape for error types and a success shape for everything else. Callers of a function returning Result<T> can then tell the two apart with the ok field (Topic 4.5, discriminated unions).
06The catch inside functions
When T is still a blank (inside a generic function), the compiler cannot pick a branch. It refuses any return whose type is not the whole deferred conditional type, with TS2322.
The fix: name the conditional type, return an honest as cast inside the body, and let callers get the exact answer. The cast is the price of asking the compiler to decide at a time when it cannot know.
type IdOf<T extends string | number> = T extends string ? string : number;
function shout<T extends string | number>(x: T): IdOf<T> {
return (typeof x === "string" ? x.toUpperCase() : Number(x) * 2) as IdOf<T>;
}
const a = shout("abc"); // string
const b = shout(21); // numberTry it yourself
- 1
Add a case to Describe
In the second example, add a branch so that
Describe<undefined>gives"nothing". Predict where to put it (before or after thebooleanquestion?) and check with a new// ^?query.It can go anywhere before the final fallback, because
undefineddoes not fitnumber,stringorboolean. - 2
Peel two layers with Flatten
In the first example, add
Flatten<string[][]>andFlatten<Flatten<string[][]>>with a// ^?each. Predict both before you check.The first gives
string[]and the secondstring: one use ofT[number]peels one array layer. - 3
Break the cast
In the third example, change
as IdOf<T>toas string. A newTS2322error appears on thereturnline: a plainstringis still not the deferred conditional type. The linesconst aandconst bstill saystringandnumber, because callers only read the declared return typeIdOf<T>.So the cast has to name the conditional type itself. That is the one place where you promise the compiler something it cannot check.
Code & diagrams
Read each answer in the Types panel. Then change IsString<42> to IsString<"a" | 42> and predict the result before you look (Topic 9.2 explains it).
Console
(nothing was printed)RangeError fits the shape of Error, so it takes the first branch. Try Result<string> and Result<Error | string> and read what you get.
Console
(nothing was printed)Callers get exact types (string for "abc", number for 21). Inside wrong(), TypeScript will not trust a plain string or number, because it cannot yet tell which branch T will take. The code still runs, since the types are erased.
Console
(nothing was printed)Expected console output
ABC 42Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Return a plain value for a conditional return type
In the third example, look at wrong: it declares a conditional return type and returns a string literal and a number with no cast.
Myth vs fact
Myth
extends in a conditional type means the same as extends in a class.
Fact
No. Here it asks "is the left type assignable to the right type?". A literal type like "hello" extends string, and { a: 1; b: 2 } extends { a: number }. Nothing is inherited.
Myth
A conditional type runs when the program runs.
Fact
No. It is computed by the compiler while it checks your code and leaves nothing in the JavaScript output. It cannot read a variable's value, only a type.
Myth
TypeScript will check my function body against each branch separately.
Fact
No. With a generic T it can't know the branch, so it checks against the unresolved conditional type, which no ordinary value fits. That is why a cast or an overload is needed.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The check
T extends Uuses assignability, not identity, so{ a: 1; b: 2 }extends{ a: number }. To test for "exactly the same type" you need a trick with two generic functions, and even that has edge cases (any, intersections). Topic 10.6 shows it when we test types. - ▸
A conditional type with an unresolved
Tis stored as a deferred conditional type object. It resolves the momentTbecomes concrete, which is whyshout("abc")returnsstringwhilewrong's body sees the raw conditional. IfTisany, the result is the union of both branches:type Yn<T> = T extends string ? "yes" : "no"gives"no" | "yes"forYn<any>. That is a special rule worth remembering when you meetanyin a generic. - ▸
Conditional types are cached per set of type arguments. They are cheap one at a time, but deeply nested ones in a generic-heavy codebase add up in the type-check time. When a build slows down, an unhelpfully clever conditional type is a common suspect (Topic 10.6).
Remember this
- 1
The problem this solves: sometimes the type of a result depends on the type of the input. A function that gives back a
stringwhen you give it a string and anumberwhen you give it a number is an example. With only the tools from earlier phases you would have to write the answer asstring | numberand lose the link between input and output, or repeat the same type by hand for each case. A conditional type keeps that link, in one place. - 2
The syntax is
T extends U ? X : Y. Read it as an ordinary question: "doesTfit insideU? If yes, the result isX, otherwise it isY." The wordextendshere does not mean inheritance. It means is assignable to, the same test the checker uses when you assign one value to a variable of another type (Topic 3.5). So"hello" extends stringis yes, and42 extends stringis no. - 3
A conditional type almost always lives inside a generic type alias (a type with a type parameter, like
type IsString<T> = ...). The parameterTis a blank the caller fills in.IsString<"hello">fills the blank with"hello", the compiler asks the question, and the answer istrue. You never call it like a function: you write it where a type is expected and the compiler computes it. - 4
You can chain conditions: put another conditional type in the "no" branch, like
if,else if,else. Always end the chain with a plain fallback type. The compiler checks branches in order and takes the first one that fits, so put the narrow cases before the wide ones. - 5
Conditional types are best at reshaping a type based on its shape.
Flatten<T>turnsnumber[]intonumberand leavesbooleanalone.Result<T>can give an error shape forErrortypes and a success shape for everything else. This is how the built-in utilities work: you will see in Topic 10.2 thatNonNullable,ExcludeandReturnTypeare only a few lines each. - 6
Generic functions are where it gets tricky. Inside a function body,
Tis still an unknown blank, so the compiler cannot decide which branch applies. Such a conditional type is deferred (put on hold). If you annotate a function's return type asT extends string ? string : number, TypeScript rejects everyreturnstatement in the body, because no concrete value is known to match a branch that has not been picked yet. The usual fix is one honestascast on the return, with a name for the conditional type so callers still get the exact answer. - 7
A conditional type is a type-level decision, so it cannot see runtime values. It decides from types only. When you need a different result for different argument types, an overload (Topic 2.5) is often simpler and is checked properly inside the body. Reach for a conditional return type when the output type is a clean function of the input type and you have more than a couple of cases.
Explain it without notes
What does T extends U ? X : Y mean, and what is extends testing?
How does a conditional type differ from an if statement?
Why does TypeScript reject return "text" in a function whose return type is T extends string ? string : number?
When would you choose a conditional return type, and when an overload?
Practice
Write type Nullable<T> = ... that is true when null fits in T and false otherwise. Test it on string, string | null and null.
Write type Wrap<T> = ... that gives T[] when T is not already an array and T itself when it is. Test it on number and number[].
Write a generic function double<T extends string | number>(x: T) whose return type is string for strings (the text repeated twice) and number for numbers (doubled). Use a named conditional type and one cast.
Trade-offs
- ↔
A conditional return type gives callers exact types with a single signature. The price is a cast in the body that the compiler cannot check, so a wrong branch there is a bug nobody catches. Overloads trade a few extra lines for a body that is checked.
- ↔
Chained conditional types are compact but hard to read. Name each helper and add a hover example (
// ^?) in a comment so the next reader can see the answer.
Done when you can
I can read
T extends U ? X : Yaloud as an if / else on types.I can explain that
extendshere means "is assignable to", not inheritance.I can build a chained conditional type with a plain fallback.
I can use
// ^?to check what a conditional type resolves to.I can explain why a generic function needs a cast for a conditional return type.