A conditional with a bare type parameter on the left, such as T extends string ? T : never, distributes over a union. Substitute each member separately and join the results. For string | number, substitute T twice: the string branch returns string, and the number branch returns never. Joining them gives string | never, which simplifies to string. Here never means no possible value, extending the exhaustiveness meaning you met in intermediate.
If the question is about the whole union, wrap both sides in tuples: [T] extends [string] ? true : false. The tuple wrapping prevents this per-member distribution. It does not create an array at runtime; this is entirely type syntax.
Work through a concrete substitution before memorizing a trick. KeepStrings<string | number> filters members. AllStrings<string | number> asks whether every possible member fits string and returns false. Built-in Extract and Exclude perform related filtering; use the standard names in application code once you understand their behavior.
A filter that accidentally discards an alternative can make a public API look narrower than the data actually is. Write both positive and negative compile cases when the distinction matters. Day 4 builds an event union with a different tool—mapped types followed by indexed access. Not every operation involving a union is conditional-type distribution.
Predict, check, and run
Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.
// A bare T checks each union member separately.
type KeepStrings<T> = T extends string ? T : never;
// The tuple wrapper checks the union as one whole type.
type AllStrings<T> = [T] extends [string] ? true : false;
const selected: KeepStrings<string | number> = "Types";
const whole: AllStrings<string | number> = false;
console.log(selected, whole);Enable JavaScript to edit and type-check this example.
Check your prediction
Types false Read the compiler error
Predict the rejected relationship before checking. Read the real diagnostic and its source location; fix the contract or the value instead of hiding it with an assertion.
type KeepStrings<T> = T extends string ? T : never;
const title: KeepStrings<string | number> = 25;
console.log(title);Enable JavaScript to edit and type-check this example.
Check your diagnosis
Filtering leaves string, so a number is not accepted. Restore a string rather than asserting the filtered result.
Substitute each member before simplifying the union
A distributive conditional runs its type-level test separately for each member of a union. Write those substitutions out before compressing the expression. Never contributes no values, so removing it from a union does not create a fallback runtime value.
A wrapped conditional asks one question about the whole input type. Use a concrete mixed union to distinguish the two designs; testing only string or only number makes the difference invisible. The declarations below use assignments to let the checker verify your predicted types.
type KeepText<T> = T extends string ? T : never;
const a: KeepText<"ready" | 25 | "done"> = "ready";
const b: KeepText<"ready" | 25 | "done"> = "done";
console.log(a, b);Enable JavaScript to edit and type-check this example.
Check your prediction
ready done The string members survive and the numeric member becomes never. The result is a literal union, not every possible string.
type Each<T> = T extends string ? true : false;
type Whole<T> = [T] extends [string] ? true : false;
const one: Each<string | number> = true;
const another: Each<string | number> = false;
const all: Whole<string | number> = false;
console.log(one, another, all);Enable JavaScript to edit and type-check this example.
Check your prediction
true false false Each yields true|false because its members take different branches. Whole yields only false. A boolean-looking output does not reveal this distinction without the annotated assignments.
Try the rule on new inputs
Try assigning true to Whole<string|number> and numeric text to KeepText. Explain the failed assignment by expanding the conditional, not by memorizing the error message.
Build it yourself
Union Distribution: practise
Write KeepNumbers<T> to retain numeric members, plus AllNumbers<T> to test a whole union. Print 25 and false for number|string. Do not use a runtime cast.
// Start from the stated contract. Add a valid call and an edge case.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
A filter returns T or never; a whole-union predicate returns literal booleans.
Hint 2: a first step
Only the whole-union test should wrap T in a tuple.
Show one solution
type KeepNumbers<T> = T extends number ? T : never;
type AllNumbers<T> = [T] extends [number] ? true : false;
const chosen: KeepNumbers<number | string> = 25;
const all: AllNumbers<number | string> = false;
console.log(chosen, all);Enable JavaScript to edit and type-check this example.
25 falseTrace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.
Change it and test it
Test number alone and number|string first. Write the result of each branch before checking.
Going further
Try never itself only after the ordinary union cases make sense. Never is the empty union: a distributive filter has no members to process, so produces never; the wrapped test instead checks assignability of the whole tuple. This edge case is optional; later exercises use concrete event maps and do not depend on it.
Completion is saved in this browser only. It does not measure mastery.