Intermediate1 questionFull Guide

TypeScript Union & Intersection Types — Complete Interview Guide

Master TypeScript union and intersection types — how | and & work, discriminated unions for safe branching, structural composition with intersections, and the real semantic difference between OR-typed and AND-typed shapes.

The Mental Model

A union type (A | B) is a type that holds exactly one of several options at any given moment — like a box that contains either a spanner or a screwdriver, but you don't know which until you look inside. An intersection type (A & B) holds all of the options simultaneously — like a Swiss Army knife that has both a blade AND a screwdriver AND scissors all at once. When you narrow a union, you figure out which one is in the box. When you use an intersection, you expect every tool to be present immediately.

The Explanation

Union Types — A | B

A union type means a value can be one of the listed types. TypeScript only allows you to access properties that are common to all members — you must narrow before accessing member-specific properties:

type StringOrNumber = string | number;

function double(value: StringOrNumber): StringOrNumber {
  // Only properties that exist on BOTH string AND number are accessible
  // value.toString() — OK, both have toString()
  // value.toUpperCase() — Error! number has no toUpperCase()
  // value.toFixed() — Error! string has no toFixed()

  if (typeof value === "string") {
    return value.repeat(2); // Narrowed to string — string methods available
  }
  return value * 2; // Narrowed to number — arithmetic available
}

// Literal unions — a very common pattern for enumerating options
type Direction = "north" | "south" | "east" | "west";
type Status = "idle" | "loading" | "success" | "error";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

Discriminated Unions — The Most Powerful Pattern

Add a shared literal property to every union member to create a discriminated union. TypeScript can then narrow the entire shape based on that one property's value:

// Each member has a unique 'type' literal — this is the discriminant
type Action =
  | { type: "INCREMENT"; amount: number }
  | { type: "DECREMENT"; amount: number }
  | { type: "RESET" }
  | { type: "SET_USER"; user: User };

function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "INCREMENT":
      // action: { type: "INCREMENT"; amount: number }
      return { ...state, count: state.count + action.amount };
    case "RESET":
      // action: { type: "RESET" } — no amount property accessible
      return initialState;
    case "SET_USER":
      // action: { type: "SET_USER"; user: User }
      return { ...state, user: action.user };
    default:
      return state;
  }
}

// Discriminated unions also work with if/else — the discriminant can be any literal
type Shape =
  | { kind: "circle";  radius: number }
  | { kind: "square";  side: number }
  | { kind: "rect";    width: number; height: number };

function area(s: Shape): number {
  if (s.kind === "circle") return Math.PI * s.radius ** 2;
  if (s.kind === "square") return s.side ** 2;
  return s.width * s.height; // TypeScript knows kind === "rect" here
}

Intersection Types — A & B

An intersection type means a value must simultaneously satisfy all of the listed types. The result type has all properties from all members:

interface HasId   { id: number; }
interface HasName { name: string; }
interface HasTimestamps { createdAt: Date; updatedAt: Date; }

// An intersection: must have ALL properties from ALL three interfaces
type BaseEntity = HasId & HasName & HasTimestamps;

const user: BaseEntity = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
  updatedAt: new Date(),
}; // All three groups must be present

// Practical pattern: extending a third-party type without interface merging
import { Request } from "express";
type AuthRequest = Request & { user: User; sessionId: string };

function protectedHandler(req: AuthRequest, res: Response) {
  console.log(req.user.name);    // user is typed — from intersection
  console.log(req.headers.host); // headers is typed — from Request
}

Property Conflicts in Intersections

When intersected types have the same property name with incompatible types, the result is never — a silent footgun:

type A = { value: string };
type B = { value: number };
type AB = A & B;
// AB['value'] is string & number = never

const x: AB = { value: ??? }; // Impossible — nothing is both string and number
// TypeScript will not error here at definition time — the error appears when you try
// to assign a value: string is not assignable to never

Union vs Intersection — The Structural Perspective

Counterintuitively, union types have fewer members accessible (only the intersection of their property sets) while intersection types have more members accessible (the union of all property sets):

interface Cat { meow(): void; name: string; }
interface Dog { bark(): void; name: string; }

// Union Cat | Dog — only 'name' is accessible without narrowing
// (it's the only property that exists on BOTH Cat AND Dog)
function greet(pet: Cat | Dog) {
  pet.name;   // OK — both have name
  pet.meow(); // Error — Dog has no meow()
  pet.bark(); // Error — Cat has no bark()
}

// Intersection Cat & Dog — ALL properties from both are accessible
function greetSuperPet(pet: Cat & Dog) {
  pet.name;   // OK
  pet.meow(); // OK — must be a Cat
  pet.bark(); // OK — must also be a Dog
}

Common Misconceptions

⚠️

Many developers think union types allow access to all member properties — union types only allow access to properties that exist on every member. You must narrow to a specific member to access unique properties.

⚠️

Many developers think intersection types produce fewer properties than their constituents — the opposite is true. An intersection A & B has all properties from A plus all properties from B. It's wider, not narrower.

⚠️

Many developers think conflicting properties in intersections cause a compile error at definition time — TypeScript silently produces never for the conflicting property. The error only surfaces when you try to assign a value to that property.

⚠️

Many developers use discriminated unions only for error handling or state machines — they are equally valuable for any heterogeneous collection: API action types, DOM event variants, form field configurations, and router route descriptors.

⚠️

Many developers think string literal unions are just documentation — they are enforced at compile time. Passing 'norht' (typo) to a Direction parameter is a compile error, not a runtime surprise.

⚠️

Many developers think intersections and extends are exactly equivalent — interface extends errors immediately on conflicting properties. Type intersections silently produce never. This difference matters when composing types from external sources.

Where You'll See This in Real Code

→

Redux action types: every Redux codebase models actions as discriminated unions. Each action has a unique type literal, and reducers switch on it — TypeScript narrows to the exact action shape in each case, eliminating all action.payload as Type casts.

→

React component variants: a Button component might accept type: 'primary' | 'secondary' | 'danger' as a literal union prop — TypeScript enforces valid variants at every call site with no runtime cost.

→

Express middleware composition: AuthRequest = Request & { user: User } is the standard pattern for typed middleware that adds properties to the request — the intersection combines the library type with your application's custom additions.

→

API result modeling: ApiResponse<T> = { ok: true; data: T } | { ok: false; error: ApiError } is a discriminated union that forces every consumer to handle both success and failure before accessing the data.

→

GraphQL fragment composition: tools like GraphQL Code Generator use intersection types to model fragments — a query result type is the intersection of multiple fragment types, ensuring all queried fields are available.

→

Event handler unions: addEventListener callback types use union literals for the event name to provide fully typed events — 'click' gives MouseEvent, 'keydown' gives KeyboardEvent — powered by overloads and union mapping in TypeScript's DOM lib.

⚡

Interview Cheat Sheet

  • ✦Union A | B — value is ONE of the listed types; only shared properties accessible without narrowing
  • ✦Intersection A & B — value satisfies ALL listed types; all properties from all members are accessible
  • ✦Literal union: type Status = 'a' | 'b' | 'c' — exhaustive compile-time enumeration of string/number literals
  • ✦Discriminated union: shared literal property (kind, type, status) enables switch-based narrowing
  • ✦Union property access: only properties present on ALL members are safe without narrowing
  • ✦Intersection property conflict: same key with incompatible types → that property becomes never
  • ✦extends vs &: extends errors on conflicts; & silently produces never
  • ✦Use union for OR semantics (one of these), intersection for AND semantics (all of these)
  • ✦Discriminated unions are the TypeScript-native alternative to class hierarchies for polymorphism
💡

How to Answer in an Interview

  • 1.The counterintuitive property access rule is a key differentiator: 'A union of two types has fewer accessible properties — only those common to all members. An intersection has more — everything from all members combined. It feels backwards but follows from type safety.'
  • 2.Lead discriminated union explanations with the real-world anchor: 'I model API responses, Redux actions, and state machines as discriminated unions. A shared literal property lets TypeScript narrow the entire shape — no casting, no runtime type checks beyond the one switch.'
  • 3.When asked about union vs intersection for composition, give the semantic test: 'If the value should be ONE of several things, it's a union. If the value must be ALL of several things simultaneously, it's an intersection.'
  • 4.The never footgun in intersections is a senior signal: 'Conflicting property types in an intersection silently produce never for that property — TypeScript doesn't error at definition time. This is subtle and dangerous when intersecting types from external libraries you don't control.'

Practice Questions

1 question
#01

What are union types and intersection types? When do you use each?

EasyType System💡 Union = A OR B (use narrowing to distinguish); intersection = A AND B (merged shape)

Related Topics

TypeScript Types vs Interfaces — Complete Interview Guide
Intermediate·8–12 Qs
TypeScript Utility Types — Complete Interview Guide
Intermediate·6–10 Qs
TypeScript Conditional Types — Complete Interview Guide
Advanced·6–10 Qs
TypeScript Type Guards & Narrowing — Complete Interview Guide
Intermediate·6–10 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz