TypeScript: Union and Intersection Types
One of the common core questions in a TypeScript interview is:
Explain the differences between Union (
|) and Intersection (&) types in TypeScript. How do they behave when applied to primitive types vs. object types?
Unions and intersections allow you to compose complex types out of simpler types. However, their behavior changes depending on whether they are combining primitives (like strings/numbers) or object types (interfaces).
1. Union Types (|)
A union type represents a value that can be one of several types. It is created using the pipe (|) symbol.
type ID = string | number;
let userId: ID;
userId = "abc"; // ✅ Allowed
userId = 123; // ✅ Allowed
userId = true; // ❌ Compile Error: Type 'boolean' is not assignable to type 'ID'.Union behavior on Objects:
If you have a union of two object shapes, you can only safely access properties that are common to both types unless you perform type narrowing.
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getAnimal(): Bird | Fish { ... }
const pet = getAnimal();
pet.layEggs(); // ✅ Allowed (Common to both)
pet.swim(); // ❌ Compile Error (Only on Fish)2. Intersection Types (&)
An intersection type combines multiple types into one type that has all the features of each. It is created using the ampersand (&) symbol.
interface Serializable {
serialize(): string;
}
interface Loggable {
log(): void;
}
type OutputModel = Serializable & Loggable;
const model: OutputModel = {
serialize() { return "data"; },
log() { console.log("Logged"); }
}; // ✅ Must implement both functions!Intersection behavior on Primitives:
If you intersect two incompatible primitive types, you get never, because a value cannot be both a string and a number simultaneously:
type Impossible = string & number; // Type resolves to 'never'3. Discriminated Unions
A common architectural pattern in TypeScript is the Discriminated Union (also called Tagged Unions). It is used to create clear, type-safe conditional logic by adding a common literal property (the "discriminant") to each member of the union.
interface SuccessResponse {
status: "success"; // Discriminant
data: string[];
}
interface ErrorResponse {
status: "error"; // Discriminant
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
if (response.status === "success") {
// TypeScript knows it is SuccessResponse here
console.log(response.data);
} else {
// TypeScript knows it is ErrorResponse here
console.log(response.message);
}
}Key Contrast Summary
| Behavior Feature | Union (|) | Intersection (&) |
| :--- | :--- | :--- |
| Logic Meaning | logical OR (either type A OR B) | logical AND (both type A AND B) |
| Primitives | Creates a choice (string \| number) | Creates never (string & number) |
| Object Properties | Only common properties are accessible | All properties of both types are accessible |
| Common Use Case | Representing alternative states | Extending/combining object shapes |
Key Takeaways
- Union Types: Created using
|to allow a value to be one of several types, requiring narrowing to access non-common properties. - Intersection Types: Created using
&to combine multiple types into a single type containing all members. - Primitive Intersection: Intersecting incompatible primitive types like
string & numberresults in thenevertype. - Discriminated Unions: Leverage a literal discriminant property to perform clean, type-safe conditional logic.