What is union in TypeScript
Union type in TypeScript lets a variable hold one of several specified types, declared with the | operator. TypeScript tracks which type is active at each point and requires you to narrow before accessing type-specific members.
Theory
TL;DR
- Think multi-tool: the value is a knife or a screwdriver or pliers, one at a time, but you carry options.
- Union gives a choice (
A | B), while intersection gives a combination (A & B). - TypeScript narrows the union type automatically inside
typeof,instanceof, or literal checks. - Use union for 2-3 known alternatives; switch to a discriminated union for complex branching.
Quick example
type Id = string | number;
let userId: Id = "user-123"; // OK: string
userId = 456; // OK: number
userId = true; // Error: boolean not in union
// TypeScript narrows the type inside the branch
if (typeof userId === "string") {
console.log(userId.toUpperCase()); // string methods available here
}After the check, TypeScript knows userId is a string inside that block. Outside, it stays string | number.
Key difference
Union gives the compiler a choice: the value is exactly one of the listed types, not a combination of all of them. To use type-specific methods or properties, you narrow first with typeof, instanceof, or a literal check. Intersection (&) works the other way: the value must satisfy all types at once.
When to use
- API response field can be
string | null→ union. - Config option accepts
number | boolean→ union. - Event data varies by type → discriminated union with a shared
kindfield. - Four or more alternatives with complex narrowing → consider branded types.
How TypeScript handles this
At compile time, TypeScript tracks which types are still possible at each point in the code. This is called control flow analysis. When you write if (typeof x === "string"), the compiler narrows x to string inside that block and removes that branch from the remaining type. At runtime, none of this exists. Union types are fully erased after compilation, leaving plain JavaScript values.
Common mistakes
Accessing methods without narrowing first:
let id: string | number = "abc";
id.toFixed(); // Error: toFixed does not exist on stringFix: if (typeof id === "number") id.toFixed();
Overly broad primitive union:
function process(value: string | number | boolean) {
value.length; // Error: length does not exist on number | boolean
}No shared methods across all three. Either narrow explicitly or restructure with a discriminated union.
Expecting intersection behavior from a union:
type A = { a: string };
type B = { b: number };
let x: A | B = { a: "hi" };
x.b; // Error: might be type A, which has no bA | B means one of, not both. For overlap, use A & B.
Forgetting null from fetch responses:
async function getName(): Promise<string> { // Wrong
const res = await fetch("/name");
return res.json(); // Actually Promise<string | null>
}Fix: type it as Promise<string | null> and add a null check before returning.
Real-world usage
- React:
ReactNode = null | string | number | ReactElement(the type ofchildren). - Express: route params typed as
string | undefined. - Node.js:
fs.readFilecallback receivesNodeJS.ErrnoException | nullas the first argument. - tRPC: discriminated unions for typed API error responses.
Follow-up questions
Q: What is a discriminated union?
A: A union where each member has a shared literal field like kind: "not-found". TypeScript narrows automatically in if or switch based on that field, no extra checks needed.
Q: Union vs any: what is the difference?
A: any disables type checking entirely. Union keeps it: you must narrow before accessing type-specific members, so mistakes show up at compile time.
Q: Can unions nest?
A: Yes. (string | number[]) | boolean is valid. The compiler flattens it internally for checking.
Q: Any runtime performance cost?
A: None. Union types are erased at compile time and exist only during static analysis.
Q: How does control flow analysis work with the in operator in TypeScript 4.9+?
A: if ("prop" in obj) narrows obj to the shapes that include that property. Given {a: 1} | {b: 2}, after if ("a" in obj) TypeScript knows obj has a. Useful when there is no shared discriminant field.
Examples
Optional image source in a React component
A prop that accepts a loaded URL or null as a fallback.
interface ImageProps {
src: string | null; // loaded image or placeholder
alt: string;
}
function Image({ src, alt }: ImageProps) {
return (
<img
src={src || "placeholder.png"} // null handled safely
alt={alt}
/>
);
}
<Image src="photo.jpg" alt="Cat" />; // string
<Image src={null} alt="No image yet" />; // null → placeholderThe union makes the optional state explicit. Without null in the type, TypeScript would reject the second call at compile time.
Discriminated union for API error handling
When an API returns different error shapes, a kind field gives TypeScript enough information to narrow automatically.
type ApiError =
| { kind: "validation"; message: string; field: string }
| { kind: "not-found"; id: number }
| { kind: "server"; status: number };
function handleError(error: ApiError) {
if (error.kind === "validation") {
console.log(error.field); // OK, narrowed to validation shape
} else if (error.kind === "not-found") {
console.log(error.id); // OK, narrowed to not-found shape
}
// error.message outside a branch → Error: not all members have it
}I have seen this break when a developer adds a new union member but forgets to handle it in the chain. A final else that throws makes the gap visible at runtime. For exhaustive compile-time checking, a switch with a never assertion in the default case is even cleaner.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.