Typorithm · hard

3. Conditional types and distribution

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

3. Conditional types and distribution

0%

Concept and contract

Distinguish per-member transformations from tests of an entire union.

A conditional type chooses a branch based on assignability: T extends U ? X : Y. When the checked side is a naked type parameter, instantiating it with a union distributes the transformation over individual members. This is often useful for filtering, but surprising when testing the union as a whole.

Wrapping both sides in a tuple suppresses that distribution. Decide whether the question is about every member independently or the complete input set. Write a small example with mixed members before hiding the conditional behind several utility aliases.

Mechanism and worked example

The example compares distributed ToArray with WholeArray.

The example compares distributed ToArray with WholeArray. For string | number, the first becomes string[] | number[]; the second becomes (string | number)[]. These admit different runtime collections even though both mention the same two primitive types.

never behaves like an empty union in a distributed conditional, so a naive T extends never check does not return the expected true branch for T = never. A tuple-wrapped [T] extends [never] test asks about the entire type and avoids this trap.

Complete worked example

typescript

type ToArray<T> = T extends unknown ? T[] : never;
type WholeArray<T> = [T] extends [unknown] ? T[] : never;
const separate: ToArray<string | number> = [1, 2];
const mixed: WholeArray<string | number> = [1, "two"];
Reasoning
The naked parameter distributes; tuple wrapping asks about the union as one type.

Counterexamples and common mistakes

The rejected assignment mixes values in an array required to be either all strings or all numbers.

The rejected assignment mixes values in an array required to be either all strings or all numbers. A broad array of the union would accept that mixture. The compiler is enforcing a correlation about the collection, not merely checking each element in isolation.

Do not use conditional types to pretend runtime branching occurs. Both branches disappear after compilation. If a function’s output changes with an input value, its implementation must still branch and its public signature must accurately connect the input and result.

Rejected counterexample: expected compiler diagnostic

typescript

type ToArray<T> = T extends unknown ? T[] : never;
// @ts-expect-error Neither string[] nor number[] admits this mixture.
const invalid: ToArray<string | number> = [1, "two"];
Reasoning
Mixed elements violate the distributed union of homogeneous arrays.

Independent practice

Implement an IsNever helper and a distributed OnlyStrings helper.

Implement an IsNever helper and a distributed OnlyStrings helper. Verify never, a mixed union and a nonstring primitive. Then write a whole-union predicate that rejects string | number as not entirely string.

Use type-level receiving assignments to true or false as regression checks. No runtime helper is needed for these transformations; the exercise is about static relationships. Explain which expressions distribute and why the tuple wrappers matter.

Step 1: Derive each result for one union member. Step 2: Wrap the whole-type predicates in tuples.

Practice starter: solve before reading the solution

typescript

type IsNever<T> = unknown;
type OnlyStrings<T> = unknown;
type EntirelyString<T> = unknown;
Reasoning
Test never and a mixed union to reveal the difference between distribution and whole-type checks.
  • Hint 1: never is an empty distributed union.
  • Hint 2: OnlyStrings should return never for rejected members.

Solution, tests and trade-offs

The solution wraps the never and whole-union tests, but leaves OnlyStrings distributive.

The solution wraps the never and whole-union tests, but leaves OnlyStrings distributive. Returning never for rejected members removes them from the resulting union. This pattern underlies built-in Exclude and Extract, which the utilities lesson examines directly.

Test the empty union and a mixed union, not only a single primitive. Include a rejected assignment that would succeed if your distributed result accidentally became a whole-union array. These small counterexamples are more informative than a deeply nested type puzzle.

Practice solution and acceptance checks

typescript

type IsNever<T> = [T] extends [never] ? true : false;
type OnlyStrings<T> = T extends string ? T : never;
type EntirelyString<T> = [T] extends [string] ? true : false;
const a: IsNever<never> = true;
const b: OnlyStrings<"a" | 2> = "a";
const c: EntirelyString<string | number> = false;

// Acceptance checks
if(!a||b!=="a"||c!==false)throw new Error("type assertions");
Reasoning
Rejected members become never and disappear; wrapped predicates inspect the complete input.

Interview reasoning and next step

For interviews, derive the result one union member at a time, then compare the tuple-wrapped version.

For interviews, derive the result one union member at a time, then compare the tuple-wrapped version. Explain why never disappears in a distributed transformation without claiming that all conditional types distribute automatically.

Next, infer extracts structure inside the true branch. It adds pattern matching to conditional types, but the same distribution and assignability rules still determine what the pattern can observe.

Optional stretch — after the core task works: Compare distributed and whole-union arrays with a mixed value.

Distinguish per-member transformations from tests of an entire union.