Articles
TypeScript Conditional Type Interview Questions: Distribution, infer, and Edge Cases
Practice TypeScript conditional type interviews: derive union distribution, infer payloads, handle never and unknown, and verify contracts with compiler tests.
A TypeScript conditional type interview becomes much easier when you identify what the compiler is checking: each member of a union, or the union as a whole. Then explain what the successful branch can extract and what happens to inputs such as never, any, and unknown. Memorizing T extends U ? X : Y is only the starting point.
This guide develops that reasoning through a small event model, a distribution comparison, and a test matrix. Official facts come from TypeScript documentation. Candidate-report boundary: the practice records linked below do not establish how often employers ask conditional types. Interview recommendations and exercises are our own preparation framework, not a private hiring rubric.
For each exercise, explain the relationship your types preserve before reaching for an assertion.
What does a conditional type actually test?
A conditional type selects a result through a type relationship. The extends in its condition is an assignability check, not a runtime instanceof test. A type alias does not inspect a network response or execute a JavaScript branch.
Start with an original example:
type ShippingLabel<T> = T extends { trackingId: string }
? "tracked"
: "untracked";
type A = ShippingLabel<{ trackingId: string; carrier: string }>;
// "tracked"
type B = ShippingLabel<{ reference: string }>;
// "untracked"
The extra carrier property does not invalidate the first match. The condition asks whether the supplied structure satisfies the required shape. It does not demand identical property lists.
There is also a design distinction between rejecting an input and giving it a fallback result. A generic constraint such as T extends { trackingId: string } limits accepted type arguments. A conditional inside an unconstrained alias can accept other inputs and return "untracked". The conditional types handbook explains this constraint-versus-fallback distinction.
In an interview, state the contract first: “Should unsupported input fail at the call site, or remain valid and map to a fallback?” Those choices create different APIs even when the successful examples look identical.
When does a conditional type distribute over a union?
Official rule: a conditional whose checked type is a naked type parameter distributes over union members when instantiated. The TypeScript 2.8 release notes describe that expansion explicitly.
type TrackedOnly<T> = T extends { trackingId: string } ? T : never;
type Shipment =
| { kind: "parcel"; trackingId: string }
| { kind: "pickup"; reference: string };
type Tracked = TrackedOnly<Shipment>;
// { kind: "parcel"; trackingId: string }
Explain this in three steps. Substitute the parcel member, which survives. Substitute the pickup member, which becomes never. Combine the results; never contributes no possible member to the resulting union.
Do not say that every expression containing a union distributes. A direct check of string | number extends string evaluates the union together. Likewise, checking a property access such as T["value"] does not make that expression a naked type parameter. Distribution depends on the form of the checked type, not the mere presence of a union somewhere nearby.
For preparation, rewrite each distributive example as separate branches on paper. This makes accidental filtering visible before the compiler produces an unexpectedly narrow result.
How do tuple wrappers change the question?
Wrapping both sides in single-element tuples suppresses distribution at that conditional:
type EachString<T> = T extends string ? "yes" : "no";
type AllString<T> = [T] extends [string] ? "yes" : "no";
| Input | EachString<Input> | AllString<Input> | Reason |
|---|---|---|---|
"paid" | "yes" | "yes" | One matching literal |
"paid" or 42 as a union | "yes" or "no" | "no" | Member checks versus one combined check |
string | "yes" | "yes" | Entire input satisfies string |
never | never | "yes" | Empty distribution versus wrapped assignability |
The never row is where a memorized “all members” explanation needs care. A wrapped check does not establish that at least one value exists. If your contract requires a nonempty input type, add an explicit never guard.
Tuple wrapping is useful when validating a union collectively. Distribution is useful when mapping or filtering alternatives. Neither is universally safer. Choose based on whether callers expect one answer about the entire input or a result for each alternative.
What does infer extract, and where is it available?
infer introduces a type variable within a conditional pattern. The successful branch can use the type inferred from the matching structure. It does not generate a runtime variable or search arbitrary object data.
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type PairElement = ElementOf<readonly ["ready", 3]>;
// "ready" | 3
Here the array pattern accepts readonly arrays as well as mutable arrays. The result is the union of tuple element types; it does not preserve tuple positions. If position matters, match a tuple structure instead, such as readonly [infer First, ...unknown[]] for a required first element. An empty tuple then fails that pattern.
Our recommended explanation is: name the pattern, identify the extracted position, and show a failing input. For ElementOf, a plain number produces never; for a first-element extractor, an empty tuple does. This prevents the vague claim that infer somehow “figures out everything.”
Inference can combine multiple candidate types. The 2.8 notes explain that covariance and contravariance affect how candidates combine. Avoid applying a single “infer always returns a union” slogan to parameter positions. If a prompt reaches that level, reduce it to a small compiler example and state the relevant strictness settings.
A worked event-payload question
Suppose an original practice prompt asks you to extract the payload for a specific event kind:
type AppEvent =
| { kind: "saved"; payload: { documentId: string } }
| { kind: "failed"; payload: { message: string } }
| { kind: "closed" };
type PayloadFor<E, K> =
E extends { kind: K; payload: infer P } ? P : never;
type SavedPayload = PayloadFor<AppEvent, "saved">;
// { documentId: string }
The checked parameter is E, so the event union distributes. The saved event matches both required properties and yields its payload. The failed event has the wrong kind. The closed event has no required payload. Both rejected branches disappear as never.
Now change the requested kind to "saved" | "failed". The output becomes a union of the two payload shapes. That is appropriate for a payload extractor, but it loses the association between a particular kind and its particular payload once the kind is discarded.
If the caller must send a correlated kind-payload pair, preserve the event union itself. Extracting a kind union and a payload union independently can permit mismatched combinations in a loosely designed API. A smaller type is not automatically a more accurate contract.
Next ask about payload?: P. Our pattern requires the property, so an optional payload does not satisfy it. Changing the pattern to an optional property is a contract change, not a harmless syntax fix. Decide whether missing payloads should be rejected, represented by undefined, or handled through a separate event variant.
Finally, this extractor says nothing about JSON validity. Receiving a value typed as unknown still requires runtime validation. A conditional alias cannot verify that a remote service actually sent a document identifier.
Why never, any, and unknown need separate tests
never is useful for eliminating impossible alternatives, but a distributive check with never has no members to evaluate. Therefore this familiar detector is wrong:
type WrongIsNever<T> = T extends never ? true : false;
type IsNever<T> = [T] extends [never] ? true : false;
WrongIsNever<never> is never, while IsNever<never> is true. The wrapped form tests the supplied type together instead of mapping an empty union.
unknown represents a value whose usable structure has not been established. It is not assignable to string, so EachString<unknown> is "no". In the opposite direction, ordinary types are assignable to unknown. These directions are not interchangeable. The TypeScript 3.0 documentation describes its top-type behavior and conditional examples.
any is different again. For the naked conditional used above, EachString<any> yields "yes" | "no". Do not generalize that result to every conditional involving any; wrappers and particular conditions can produce other outcomes. Treat any as an explicit policy decision at your utility's boundary.
A practical follow-up is whether callers are allowed to supply any at all. Rejecting it, propagating it, and tolerating degraded precision are different library choices. Explain the desired behavior before adding a clever detector that future maintainers must understand.
What do overloads and recursive utilities change?
Official behavior: when extracting from an overloaded function type, inference uses the last signature rather than performing argument-based overload resolution. The handbook documents this limitation. A return-type extractor therefore cannot promise the return value associated with a particular argument just because several overloads are visible.
If that relationship matters, model the mapping explicitly or let an actual call select its overload. Do not mistake introspection of a function type for executing the compiler's full call-resolution procedure.
For recursive unwrapping, prefer understanding the standard Awaited<T> contract before writing a simplified promise extractor. The utility types documentation shows recursive unwrapping and unions. A toy Promise<infer U> example does not automatically reproduce all its thenable behavior.
Recursion also consumes compiler work. The TypeScript 4.5 release notes explain tail-recursion optimizations and remaining heuristics. Avoid promising an unlimited recursion depth or a version-independent numeric ceiling. For a library, test representative input sizes and keep the exported contract understandable.
How should you prove the answer?
Use type assertions in a dedicated compile-only fixture, alongside expected failures. For ordinary concrete types, this helper is useful:
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2) ? true : false;
type Assert<T extends true> = T;
type CheckSaved = Assert<Equal<
PayloadFor<AppEvent, "saved">,
{ documentId: string }
>>;
An equality helper is a testing technique, not a complete mathematical model of every TypeScript edge case. Combine it with assignments and expected compile errors where the caller's experience matters.
| Test category | Example input | Contract to verify |
|---|---|---|
| Matching member | Saved event | Exact payload shape |
| Mixed union | Saved and failed events | Intentional distribution |
| Missing field | Closed event | Documented fallback |
| Empty type | never | No accidental boolean claim |
| Untrusted type | unknown | No invented structure |
| Escape hatch | any | Explicitly chosen precision policy |
Record the compiler version and configuration. A successful type check proves the fixture's static expectations under that setup; it does not prove runtime validation, acceptable compiler performance at every scale, or compatibility with every older TypeScript release.
Practice the reasoning in a larger problem
The suggested conditional-type extensions are ours.
| Question | Targeted practice extension |
|---|---|
| Implement toy-language types and generic substitution | Explain substitution and conflicting type requirements |
| Review and Improve Four AI-Assisted TypeScript Pull Requests | Review a utility against accepted and rejected inputs |
| Run Promise Tasks with Bounded Concurrency | Preserve task-result relationships without overstating cancellation |
| Implement debounced autocomplete component | Model result variants and validate external input |
| Design a REST API Abstraction Layer | Keep response contracts correlated and maintainable |
Choose one Frontend Engineer practice question, write a small type contract, and explain its failure cases aloud. A convincing answer names the computation unit, derives the branches, and proves the caller-facing relationship without hiding uncertainty behind a cast.
Sources and Further Reading
- TypeScript handbook: Conditional Types
- TypeScript 2.8: distribution and inference rules
- TypeScript 3.0: unknown and conditional types
- TypeScript handbook: Utility Types
- TypeScript 4.5: Awaited and recursive type optimization
Research checked September 14, 2026. Compile examples with the TypeScript version and strictness settings used by your project.