Unions, literals and narrowing
typeof, in and discriminated unions that shrink types line by line.
Unions say "one of these"; narrowing proves "this one, here". Interviewers love this page because it is TypeScript thinking in pure form: start wide, test, and watch the type shrink inside each branch.
Real-life situation
A fetch can be loading, successful, or failed — three shapes, one
variable. Without a union you juggle flags and nullable fields; with a
discriminated union on status, each branch knows exactly which fields
exist. Accessing users inside the "success" branch is safe because
the check already proved the shape.
Visual
Try it
Checked with TypeScript 6.0.3, strict mode
type FetchState =
| { status: "loading" }
| { status: "success"; users: string[] }
| { status: "error"; message: string };
function describe(state: FetchState): string {
if (state.status === "loading") {
return "still loading";
}
if (state.status === "success") {
return state.users.length + " users";
}
return "failed: " + state.message;
}
console.log(describe({ status: "loading" }));
console.log(describe({ status: "success", users: ["a", "b"] }));
console.log(describe({ status: "error", message: "down" }));
console.log(describe({ status: "done" }));Compiler not loaded. Check or Run loads it here (about 9 MB) — nothing downloads until you do.
Try changing "done" to "error" with a message — clean. The guards
that shrink types are small: typeof for primitives, in for object
shapes, instanceof for class instances, and equality on a shared
literal field for discriminated unions. A custom type guard (value is User) teaches the checker a test you wrote yourself.
The compiler verifies each narrowed type below at build time:
function firstName(user: string | null): string {
if (user === null) {
return "guest";
}
user; // user: string
return user;
}
console.log(firstName(null));
console.log(firstName("sam"));Will it compile? — decide first, then reveal.
const direction: "up" | "down" = "left";
console.log(direction);Interview questions
What is a union type?basic
A value that can be one of several types, written with | like string | number. You use it when a variable, parameter, or response genuinely holds different shapes at different times. The compiler forces you to handle each member before using type-specific features — you cannot call string methods until you have narrowed away the number case. Keep unions small and meaningful so every branch is worth handling.
Interviewers also ask
- What must you do before calling a method on a union?
How do you narrow a union with typeof?basic
You guard with typeof value === 'string' inside an if, and the compiler shrinks the union to that branch — string in the true path, the remainder in the else. It works for primitives like string, number, and boolean. Write the check as a plain condition so it also guards at runtime. For objects, switch to in, instanceof, or a literal tag check instead.
Interviewers also ask
- What does the else branch contain after a typeof guard?
What is a discriminated union?intermediate
A union of object types that share one literal tag field, like { kind: 'circle'; radius: number } | { kind: 'square'; size: number }. You switch on shape.kind and the compiler narrows the whole object in each branch, so only the matching fields are visible. It is the clearest way to model states, events, and API variants. Keep the tag a string literal and give every member a distinct value.
Interviewers also ask
- Why must the shared field be a literal type?
How does the in operator narrow a union?basic
You test for a property with 'email' in user, and the compiler keeps only the union members that declare it. It suits object unions where members have different fields. Like typeof, the check runs at runtime too, so it doubles as real validation. Prefer a discriminated tag when you control the shapes — an explicit kind field reads better than probing for incidental properties.
Interviewers also ask
- When is a kind field better than an in check?
How does instanceof narrow a union?basic
You write if (err instanceof Error) and the compiler treats the true branch as that class, exposing its methods and fields. It works because the check consults the prototype chain at runtime. Use it for class instances like Date, Error, or your own classes. For plain object literals or interfaces, instanceof cannot help — there is no constructor to test, so prefer a tag field or in check there.
Interviewers also ask
- Why doesn't instanceof work on interfaces?
How do truthiness checks narrow a value?intermediate
Writing if (value) removes null, undefined, '', 0, and other falsy members from the type in that branch. It is concise for optional values, but be careful: legitimate 0 or empty strings are dropped too. When those values are valid, compare explicitly with value != null or value !== undefined instead. Choose the narrowest check that matches what you actually want to exclude.
Interviewers also ask
- What valid values can a bare if accidentally drop?
How do you write an exhaustive switch over a union?intermediate
You handle every member in case branches and let the default assign the remainder to never — if a member is unhandled, that assignment errors and points at the missing case. Adding a new union member then breaks the switch loudly instead of silently falling through. Keep a small assertNever helper for the default branch so the intent reads clearly and every future reader knows the switch must stay complete.
Interviewers also ask
- What does a never assignment in default prove?
What is a custom type guard?intermediate
A function whose return type is a predicate like pet is Fish, which tells the compiler the argument's narrowed type when it returns true. You implement the runtime test yourself — checking a tag or property — and the is annotation exports that knowledge to every if that calls it. Use guards to hide repeated narrowing logic behind a named, testable function. Keep the runtime test honest, since the compiler trusts your predicate.
Interviewers also ask
- What does the is keyword promise the compiler?
Key takeaways
- Model "one of" with unions; prove "this one" with a guard per branch.
- Discriminated unions (one shared literal field) scale best.
- Narrowing is compile-time only — the emitted JavaScript keeps the tests.
Runner helpers
sleep(ms)waits;fakeFetch(url)returns deterministic sample data. TypeScript is stripped before running; imports and exports are not available in the sandbox.