Tutorials Logic, IN info@tutorialslogic.com

TypeScript Union and Literal Types: Flexible but Safe Values

TypeScript Union and Literal Types

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.

What Is a Union Type?

Union Type

Union Type
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");

Why Narrowing Is Required

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

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.

Literal Options

Literal Options
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

Unions with null and undefined

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.

Nullable Union

Nullable Union
type User = {
  id: number;
  name: string;
};

function getDisplayName(user: User | null): string {
  if (user === null) {
    return "Guest";
  }

  return user.name;
}

Object Unions

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.

Object Union

Object Union
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}`;
}

Discriminated Unions

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.

Discriminated Union

Discriminated Union
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";
  }
}

Exhaustive Checks

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.

Exhaustive Switch

Exhaustive Switch
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 Arrays and Records

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.

Union Collections

Union Collections
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];
}

Common Union Design Tips

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.

  • Use literal unions instead of plain strings for fixed options.
  • Use discriminated unions for state machines and API responses.
  • Narrow before using properties or methods that only exist on one branch.
  • Avoid adding any to a union because it weakens the whole type.
  • Use exhaustive checks when every branch must be handled.
  • Prefer a shared discriminator for object unions that represent states.

TypeScript Union and Literal Types Flexible but Safe Values normal path trace

TypeScript Union and Literal Types Flexible but Safe Values normal path trace
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.

TypeScript Union and Literal Types Flexible but Safe Values edge path trace

TypeScript Union and Literal Types Flexible but Safe Values edge path trace
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.
Key Takeaways
  • Union types allow controlled alternatives.
  • Literal types restrict values to exact allowed options.
  • Narrowing is required before using member-specific behavior.
  • Use null or undefined in a union when missing values are valid.
  • Discriminated unions model UI and API states clearly.
  • Exhaustive checks help catch missing branches when a union grows.
  • Union types can drive arrays, records, roles, statuses, and option lists.
Common Mistakes to Avoid
WRONG Memorizing TypeScript Union and Literal Types Flexible but Safe Values without the situation where it is useful.
RIGHT Connect TypeScript Union and Literal Types Flexible but Safe Values to a concrete TypeScript task.
Purpose makes syntax easier to recall.
WRONG Testing TypeScript Union and Literal Types Flexible but Safe Values only with the perfect input.
RIGHT Include empty, missing, duplicate, incompatible, or failed cases when relevant.
Real bugs usually appear outside the perfect path.
WRONG Changing code before reading the visible symptom or error message.
RIGHT Inspect the output, state, configuration, or stack trace connected to TypeScript Union and Literal Types Flexible but Safe Values.
Evidence keeps debugging focused.
WRONG Memorizing TypeScript Union and Literal Types Flexible but Safe Values without the situation where it is useful.
RIGHT Connect TypeScript Union and Literal Types Flexible but Safe Values to a concrete TypeScript task.
Purpose makes syntax easier to recall.

Practice Tasks

  • Modify the example so it handles a different input or condition.
  • Write one mistake related to TypeScript Union and Literal Types: Flexible but Safe Values, then fix it and explain the fix.
  • Summarize when to use TypeScript Union and Literal Types: Flexible but Safe Values and when another approach is better.
  • Write a small example that uses TypeScript Union and Literal Types Flexible but Safe Values in a realistic TypeScript scenario.
  • Change one important value in the TypeScript Union and Literal Types Flexible but Safe Values example and predict the result first.

Frequently Asked Questions

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.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.