A union type allows a value to be one of several allowed types. Use the | symbol to join alternatives. Union types are useful when real data can arrive in more than one valid form.
Unions are common with route parameters, form inputs, API responses, UI states, feature flags, and values that start simple but become more specific after validation.
A union does not mean you can use every method from every member. Before using a member-specific method, narrow the value to the correct branch.
Add one worked example that compares the normal path with the boundary case for TypeScript Union and Literal Types: Flexible but Safe Values.
TypeScript Union and Literal Types Flexible but Safe Values should be studied as a practical TypeScript lesson, not as a label. Start by naming the input, the rule that changes the input, and the result a learner should be able to predict after reading the page.
function printId(id: string | number): void {
if (typeof id === "string") {
console.log(`String ID: ${id.toUpperCase()}`);
return;
}
console.log(`Numeric ID: ${id.toFixed(0)}`);
}
printId(101);
printId("USR-101");
When a value is string | number, TypeScript only lets you use behavior that is safe for both strings and numbers. This prevents calling toUpperCase() on a number or toFixed() on a string.
Narrowing is the process of proving which union member you currently have. Common narrowing tools include typeof, equality checks, in, discriminated unions, and custom type guards.
| Check | Best For |
|---|---|
| typeof value === "string" | Primitive unions |
| status === "success" | Literal unions |
| "data" in response | Object unions with different properties |
| isUser(value) | Reusable runtime validation |
Literal types restrict a value to exact strings, numbers, or booleans. Instead of saying a theme is any string, you can say it must be "light", "dark", or "system".
This is more precise than a plain string and catches invalid options at compile time. Literal unions are excellent for statuses, roles, modes, directions, sizes, variants, and command names.
type Theme = "light" | "dark" | "system";
type ButtonSize = "sm" | "md" | "lg";
function setTheme(theme: Theme): void {
console.log(`Theme changed to ${theme}`);
}
function buttonClass(size: ButtonSize): string {
return `btn btn-${size}`;
}
setTheme("dark");
// setTheme("blue"); // Error
A value that may be missing should say so in its type. With strict null checks enabled, User | null forces you to handle the missing case before using the user.
This is better than pretending a value always exists. It makes loading states, optional selections, and lookup failures visible in the code.
type User = {
id: number;
name: string;
};
function getDisplayName(user: User | null): string {
if (user === null) {
return "Guest";
}
return user.name;
}
Object unions are useful when each valid shape has different properties. TypeScript will only allow access to properties that are safe for the current narrowed branch.
You can narrow object unions using the in operator, a shared literal property, or a custom type guard.
type EmailContact = {
email: string;
};
type PhoneContact = {
phone: string;
};
function contactLabel(contact: EmailContact | PhoneContact): string {
if ("email" in contact) {
return `Email: ${contact.email}`;
}
return `Phone: ${contact.phone}`;
}
A discriminated union is an object union where every branch has a shared literal property. That shared property is called the discriminator. It tells TypeScript which object shape is active.
This pattern is very useful for UI loading states, API responses, payment flows, form submission states, and any workflow where each state has different data.
| State | Available Data |
|---|---|
| idle | No extra data needed. |
| loading | No extra data yet. |
| success | The loaded data is available. |
| error | An error message is available. |
type LoadState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function render(state: LoadState): string {
switch (state.status) {
case "success":
return state.data.join(", ");
case "error":
return state.message;
case "loading":
return "Loading...";
default:
return "Ready";
}
}
An exhaustive check helps ensure every union branch is handled. If you later add a new state and forget to update the switch, TypeScript can report the missing case.
This is one of the strongest reasons to use discriminated unions. Instead of discovering a missing UI state in production, you get a compiler warning while editing.
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
}
function label(state: LoadState): string {
switch (state.status) {
case "idle":
return "Idle";
case "loading":
return "Loading";
case "success":
return "Loaded";
case "error":
return "Failed";
default:
return assertNever(state);
}
}
Union types can describe array values and object values too. For example, a list of roles can be typed as UserRole[], and a lookup table can use a union as its key type.
This keeps option lists, permissions, and configuration objects aligned with the exact values the application supports.
type UserRole = "admin" | "editor" | "viewer";
const roles: UserRole[] = ["admin", "editor", "viewer"];
const labels: Record<UserRole, string> = {
admin: "Administrator",
editor: "Content Editor",
viewer: "Read Only User",
};
function labelRole(role: UserRole): string {
return labels[role];
}
A good union should model real alternatives. Avoid unions that are so broad they stop being useful, such as string | number | object | any. The more precise the alternatives, the more TypeScript can help.
For object unions, prefer a clear discriminator such as type, kind, or status. This keeps narrowing simple and makes the data shape easier to read.
1. Define the input for TypeScript Union and Literal Types Flexible but Safe Values.
2. Apply the rule from the lesson.
3. Compare the actual result with the expected result.
4. Record the fix if the result differs.
1. Try empty, missing, duplicate, or invalid data.
2. Identify where TypeScript Union and Literal Types Flexible but Safe Values changes behavior.
3. Explain the safest correction.
4. Retest the normal path.
Memorizing TypeScript Union and Literal Types Flexible but Safe Values without the situation where it is useful.
Connect TypeScript Union and Literal Types Flexible but Safe Values to a concrete TypeScript task.
Testing TypeScript Union and Literal Types Flexible but Safe Values only with the perfect input.
Include empty, missing, duplicate, incompatible, or failed cases when relevant.
Changing code before reading the visible symptom or error message.
Inspect the output, state, configuration, or stack trace connected to TypeScript Union and Literal Types Flexible but Safe Values.
Memorizing TypeScript Union and Literal Types Flexible but Safe Values without the situation where it is useful.
Connect TypeScript Union and Literal Types Flexible but Safe Values to a concrete TypeScript task.
The common mistake is memorizing syntax without understanding when the behavior changes or fails.
Remember the problem it solves in TypeScript, then attach the syntax or steps to that problem.
You can predict the result of a small example, explain a failure case, and choose it over a nearby alternative for a clear reason.
They often copy the syntax but skip the state, input, dependency, selector, route, type, or configuration that controls the behavior.
Explore 500+ free tutorials across 20+ languages and frameworks.