In TypeScript, `function pick<T extends boolean>(flag: T): T extends true ? string : number { return flag ? 'yes' : 0; }` is rejected at the return statement. Why does the compiler refuse it, and how would you type this API instead?
answer
- T is still unknown in the body
- the branch cannot be chosen yet
- deferred until instantiation
- overloads move the decision outward
- the conditional return is an unchecked promise
basics
~20 sWhile T is an unresolved type parameter the conditional return type stays deferred, so the compiler cannot prove any returned value matches whichever branch will win. Fix it with overload signatures, or with an assertion inside a loosely typed implementation.
solid answer
~50 sInside the function body `T` is still an unresolved type parameter, so `T extends true ? string : number` cannot be evaluated — it is a *deferred* conditional. The returned expression has type `string | number`, and the checker will not accept that for a type whose branch is not yet decided: for the caller who instantiates `T` as `true` only `string` is legal, and the compiler cannot verify that from inside. The idiomatic fix is a pair of overload signatures — `pick(flag: true): string` and `pick(flag: false): number` — with an implementation signature typed `(flag: boolean): string | number`, which keeps the precise caller-facing contract and lets the body check normally. The alternative is to keep the conditional signature and assert inside, which works but makes the return type a promise the compiler never verifies.
code
typescript · 12 lines// Rejected: the return type stays deferred while T is unresolved.
declare function bad<T extends boolean>(flag: T): T extends true ? string : number;
// Overloads keep the precise caller-facing contract.
function pick(flag: true): string;
function pick(flag: false): number;
function pick(flag: boolean): string | number {
return flag ? 'yes' : 0;
}
const a = pick(true); // string
const b = pick(false); // numbergo deeper
Know that a conditional type can only be evaluated once its checked type is known, and that inside a generic function the type parameter is not known yet.
Explain deferral precisely: the compiler keeps the conditional unevaluated, so no concrete value can be shown to satisfy it, and describe the overload-based rewrite.
Diagnose the error without guessing, choose between overloads, a single documented assertion, or splitting the function, and state plainly that the conditional signature is an unverified promise the implementation must honour by hand.
Own the API-design call: decide when a conditional return type is worth the unsoundness it introduces for a shared library, and set the team's rule for where assertions are allowed and how they are documented and tested.
## What "deferred" means A conditional type resolves as soon as the compiler knows the checked type. Inside a generic function body it does not: `T` is a placeholder that will be filled in at each call site, and different call sites choose different branches. So the compiler leaves `T extends true ? string : number` **unevaluated** and carries it around as a type in its own right. That state is called a deferred conditional type. Deferral is not a failure mode; it is what makes generic conditional types work at all. But a deferred type is nearly opaque: almost nothing is known to be assignable to it, because whichever branch you try to satisfy, the other one might be the one that applies. ## Why the return is rejected `flag ? 'yes' : 0` has type `string | number`. For the declared return type to be satisfied, that value would have to be acceptable no matter which branch `T` selects — and it is not. A caller writing `pick(true)` is promised a `string`; handing them a `number` would be a lie. The compiler cannot prove the body always picks the branch that matches, because the correspondence between the runtime value of `flag` and the type argument `T` is something *you* know and the type system does not track. So the error is correct and unavoidable in the general case: the body reasons about a runtime value, while the return type reasons about a type parameter, and nothing connects the two. ## The one relation the checker does allow It is worth knowing the direction that *does* work. A deferred conditional type `T extends U ? X : Y` is assignable **to** some other type `Z` when both `X` and `Y` are assignable to `Z`: ```ts declare function pick<T extends boolean>(flag: T): T extends true ? string : number; function log<T extends boolean>(flag: T) { const value: string | number = pick(flag); // fine: both branches fit } ``` Because every possible outcome fits `string | number`, the compiler can relate the deferred conditional to it. Two deferred conditionals with the same checked type and the same test also relate to each other when their branches do. What has no general rule is the opposite direction — putting a concrete value *into* a deferred conditional, which is exactly what a `return` statement asks for. ## Fix one: overload signatures Move the branching out of the type and into the signature list. The public signatures stay precise; the implementation signature is ordinary and checks normally: ```ts function pick(flag: true): string; function pick(flag: false): number; function pick(flag: boolean): string | number { return flag ? 'yes' : 0; } ``` Callers get `string` for `pick(true)` and `number` for `pick(false)`, and the body is verified against `string | number` with no assertion. The tradeoff is that overloads do not compose: a caller who holds a `boolean` of unknown value, or who is generic over it themselves, only gets the union. ## Fix two: keep the conditional, assert inside When the conditional signature is genuinely worth keeping — typically because callers are themselves generic and need the relationship preserved — write the body against a loose type and assert once, in one clearly commented place: ```ts function pick<T extends boolean>(flag: T): T extends true ? string : number { const result: string | number = flag ? 'yes' : 0; return result as T extends true ? string : number; } ``` Be honest about what this costs. `as` performs no check and emits nothing; the declared return type becomes a promise the compiler enforces on every caller but never verifies on the implementation. If the body's branching and the type's branching drift apart, callers get a confidently wrong type and the bug surfaces far from here. That is why the assertion belongs on one line with a comment, not scattered through the function. ## Fix three: do not model it as one function Often the honest answer is that this is two operations wearing one name. Two functions with two return types need no conditional, no overloads and no assertion, and the call sites read better. A conditional return type earns its keep when the branch depends on a type the caller already has in hand — an element type, a key, an awaited value — not when it depends on a boolean literal someone typed at the call site. ## What to demonstrate in an interview Name the mechanism (`T` unresolved, so the conditional is deferred), explain why the checker cannot verify the return, give the overload fix, and then volunteer the tradeoff: the conditional signature is an unverified promise, and the assertion is where the soundness is being spent. Candidates who reach straight for `any` — or who blame the compiler — reveal that they have not understood which side of the boundary the guarantee lives on.
- When can a deferred conditional type be assigned to something at all?When the target accepts both branches. `T extends U ? X : Y` is assignable to `Z` if both `X` and `Y` are assignable to `Z`, since every possible outcome fits. Two conditionals sharing the same checked type and test also relate when their branches do. The reverse direction — assigning a value into a deferred conditional — has no general rule.
- What is the cost of keeping the conditional signature and asserting inside the body?The return type becomes a promise the compiler enforces on callers but never checks on the implementation. `as` performs no runtime conversion and emits nothing, so if the body's branching drifts from the type's branching, callers receive a confidently wrong type and the failure appears far from the cause.
- When is a conditional return type genuinely the right tool over overloads?When callers are themselves generic and must preserve the relationship — the branch depends on a type they already hold, such as an element type or a key. Overloads collapse to a union in that situation. For a fixed, small set of concrete argument types, overloads are simpler and keep the body checked.
saying these in an interview costs you the question
- Calls it a compiler bug rather than deferral
- Reaches for `any` to silence the error
- Assumes the conditional return type is verified inside the body
- Thinks annotating the local variable fixes it
- Says overloads and conditional return types are interchangeable