
Quick answer: A TypeScript union type such as string | number means a value can be one of several types. Narrowing is how TypeScript follows your checks — typeof, in, instanceof, equality and discriminated unions — to know which member is present at each point. Use type for unions and aliases, and interface for object shapes you expect to extend.
Table of Contents
TypeScript union types and literal types
type Id = string | number;
type Status = "idle" | "loading" | "success" | "error"; // literal union
let state: Status = "idle";
state = "loading";
// state = "done"; // error — not one of the fourLiteral unions are one of TypeScript’s most practical features. A status field that can only be one of four strings turns a runtime typo into a compile error, and editors autocomplete the options.
Narrowing TypeScript union types with typeof and in
function format(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2); // narrows to number here
}
return value.trim(); // narrows to string here
}
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(a: Cat | Dog) {
if ("meow" in a) a.meow(); // the in operator narrows
else a.bark();
}You do not annotate the narrowed type; TypeScript works it out from the guard. That is the whole idea of narrowing: ordinary JavaScript checks give the compiler enough information to follow.
Discriminated unions
The most powerful pattern is a shared literal field that identifies each variant. A switch on that field then narrows the whole object.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
case "rect": return s.w * s.h;
}
}Add a fourth variant and every switch that handles Shape becomes a compile error until you cover the new case. That exhaustiveness check is why discriminated unions are the recommended way to model states in TypeScript.
type or interface?
| Use | Choose |
|---|---|
| Union, intersection, tuple, primitive alias | type |
| Object shape you may extend or merge | interface |
| Function type | type |
| Public API of a class you implement | interface |
In practice the two overlap heavily, and a project that picks one convention and sticks to it is fine. The hard rule is that only type can express a union or a tuple, so those always use type.
Assertions and type guards
// a custom guard returns a type predicate
function isString(v: unknown): v is string {
return typeof v === "string";
}
const values: unknown[] = ["a", 1, true];
const strings = values.filter(isString); // string[]
// assertion: last resort, no runtime check
const el = document.getElementById("app") as HTMLDivElement;Prefer a real guard to an assertion. as tells the compiler to trust you and performs no check at runtime, so a wrong assertion moves the failure to production. A type predicate is safer because it pairs the claim with an actual test. Next: modules and namespaces.
To learn TypeScript properly with a mentor, Ampersand Academy teaches web development one-to-one.
Frequently asked questions
What is a union type in TypeScript?
A type that allows a value to be one of several types, written with a vertical bar. For example string or number, or a set of literal values such as a status field.
What is type narrowing?
TypeScript following a runtime check, such as typeof or in, to know which member of a union is present in that branch, so it can allow only valid operations.
What is a discriminated union?
A union of object types that each share a literal tag field, so switching on that field narrows the object to one variant and the compiler checks you handled every case.
Should I use type or interface in TypeScript?
Use type for unions, tuples and primitives, and interface for object shapes you expect other code to extend. Either is fine for plain object shapes if the project is consistent.
What is the difference between a type guard and a type assertion?
A type guard performs a real runtime check and tells the compiler the result. An assertion only tells the compiler to trust you, with no runtime test, so a wrong one fails in production.
Last updated on · Written by Dinesh Kumar R

