Master TypeScript's advanced type composition with union types, intersection types, and type guards. Learn to handle complex type scenarios, discriminated unions, and runtime type checking.
Union types, intersection types, and type guards are fundamental to TypeScript's powerful type system. They enable you to handle complex scenarios where values can be one of several types, where you need to combine multiple types, or where you need to verify types at runtime.
These features are essential for real-world applications where data comes from APIs, user input, or external sources. You'll learn to write type-safe code that handles all possible scenarios while maintaining excellent developer experience and runtime safety.
By mastering these concepts, you'll be able to model complex domain logic accurately and create robust applications that handle edge cases gracefully.
Union types allow a value to be one of several types, providing flexibility while maintaining type safety through type narrowing.
Basic Union Types:
1// Basic union type
2type StringOrNumber = string | number;
3
4function formatValue(value: StringOrNumber): string {
5 if (typeof value === "string") {
6 return value.toUpperCase();
7 }
8 return value.toString();
9}
10
11// Union with null/undefined
12type User = {
13 id: number;
14 name: string;
15 email: string | null; // Email might not be provided
16 phone?: string; // Phone is optional
17};
18
19// Function parameter unions
20function processId(id: string | number): string {
21 // TypeScript knows 'id' could be either type
22 return id.toString(); // .toString() works on both
23}
24
25// Union with literal types
26type Status = "pending" | "approved" | "rejected";
27type Priority = "low" | "medium" | "high";
28
29interface Task {
30 id: string;
31 title: string;
32 status: Status;
33 priority: Priority;
34}
35
36function updateTaskStatus(task: Task, newStatus: Status): Task {
37 return { ...task, status: newStatus };
38}Complex Union Types:
1// Union of object types
2interface Dog {
3 type: "dog";
4 breed: string;
5 barkVolume: number;
6}
7
8interface Cat {
9 type: "cat";
10 breed: string;
11 meowFrequency: number;
12}
13
14interface Bird {
15 type: "bird";
16 species: string;
17 canFly: boolean;
18}
19
20type Pet = Dog | Cat | Bird;
21
22// Function working with union types
23function describePet(pet: Pet): string {
24 // TypeScript knows all pets have 'type' property
25 switch (pet.type) {
26 case "dog":
27 // TypeScript knows this is a Dog
28 return `Dog: ${pet.breed}, bark volume: ${pet.barkVolume}`;
29 case "cat":
30 // TypeScript knows this is a Cat
31 return `Cat: ${pet.breed}, meow frequency: ${pet.meowFrequency}`;
32 case "bird":
33 // TypeScript knows this is a Bird
34 return `Bird: ${pet.species}, can fly: ${pet.canFly}`;
35 default:
36 // TypeScript ensures exhaustive checking
37 const exhaustiveCheck: never = pet;
38 throw new Error(`Unhandled pet type: ${exhaustiveCheck}`);
39 }
40}Union Type Narrowing:
1// Type narrowing with typeof
2function processValue(value: string | number | boolean): string {
3 if (typeof value === "string") {
4 // TypeScript knows value is string here
5 return value.toLowerCase();
6 }
7
8 if (typeof value === "number") {
9 // TypeScript knows value is number here
10 return value.toFixed(2);
11 }
12
13 // TypeScript knows value is boolean here
14 return value ? "yes" : "no";
15}
16
17// Type narrowing with instanceof
18class NetworkError extends Error {
19 constructor(message: string, public statusCode: number) {
20 super(message);
21 }
22}
23
24class ValidationError extends Error {
25 constructor(message: string, public field: string) {
26 super(message);
27 }
28}
29
30function handleError(error: NetworkError | ValidationError | Error): string {
31 if (error instanceof NetworkError) {
32 return `Network error (${error.statusCode}): ${error.message}`;
33 }
34
35 if (error instanceof ValidationError) {
36 return `Validation error in ${error.field}: ${error.message}`;
37 }
38
39 return `General error: ${error.message}`;
40}
41
42// Type narrowing with 'in' operator
43interface Circle {
44 kind: "circle";
45 radius: number;
46}
47
48interface Rectangle {
49 kind: "rectangle";
50 width: number;
51 height: number;
52}
53
54type Shape = Circle | Rectangle;
55
56function getArea(shape: Shape): number {
57 if ("radius" in shape) {
58 // TypeScript knows shape is Circle
59 return Math.PI * shape.radius * shape.radius;
60 }
61
62 // TypeScript knows shape is Rectangle
63 return shape.width * shape.height;
64}Array and Function Unions:
1// Union of array types
2type MixedArray = (string | number)[];
3type StringOrNumberArray = string[] | number[];
4
5function processArray(arr: StringOrNumberArray): (string | number)[] {
6 if (typeof arr[0] === "string") {
7 // TypeScript knows arr is string[]
8 return arr.map(str => str.toUpperCase());
9 }
10
11 // TypeScript knows arr is number[]
12 return arr.map(num => num * 2);
13}
14
15// Union of function types
16type EventHandler =
17 | ((event: MouseEvent) => void)
18 | ((event: KeyboardEvent) => void)
19 | ((event: TouchEvent) => void);
20
21function attachHandler(element: HTMLElement, handler: EventHandler): void {
22 // Runtime type checking needed for function unions
23 element.addEventListener("click", handler as (event: MouseEvent) => void);
24 element.addEventListener("keydown", handler as (event: KeyboardEvent) => void);
25 element.addEventListener("touchstart", handler as (event: TouchEvent) => void);
26}Intersection types combine multiple types into one, requiring values to satisfy all constituent types simultaneously.
Basic Intersection Types:
1// Basic intersection
2interface Nameable {
3 name: string;
4}
5
6interface Ageable {
7 age: number;
8}
9
10// Person must have both name and age
11type Person = Nameable & Ageable;
12
13const person: Person = {
14 name: "John Doe",
15 age: 30
16 // Must have both properties
17};
18
19// Intersection with additional properties
20interface Identifiable {
21 id: string;
22}
23
24type Employee = Person & Identifiable & {
25 department: string;
26 salary: number;
27};
28
29const employee: Employee = {
30 id: "emp-001",
31 name: "Jane Smith",
32 age: 28,
33 department: "Engineering",
34 salary: 75000
35};Mixin Pattern with Intersections:
1// Mixin types for composition
2type Timestamped = {
3 createdAt: Date;
4 updatedAt: Date;
5};
6
7type Versioned = {
8 version: number;
9};
10
11type Auditable = {
12 createdBy: string;
13 updatedBy: string;
14};
15
16// Combine mixins with base type
17interface BaseEntity {
18 id: string;
19 name: string;
20}
21
22type FullEntity = BaseEntity & Timestamped & Versioned & Auditable;
23
24function createEntity(
25 base: BaseEntity,
26 creator: string
27): FullEntity {
28 const now = new Date();
29 return {
30 ...base,
31 createdAt: now,
32 updatedAt: now,
33 version: 1,
34 createdBy: creator,
35 updatedBy: creator
36 };
37}
38
39// Usage
40const entity = createEntity(
41 { id: "ent-001", name: "Example Entity" },
42 "admin"
43);Function Intersection Types:
1// Intersection of function types
2type Logger = (message: string) => void;
3type Counter = () => number;
4
5// Function that is both a logger and counter
6type LoggerCounter = Logger & Counter & {
7 reset: () => void;
8};
9
10function createLoggerCounter(): LoggerCounter {
11 let count = 0;
12
13 const fn = ((message: string) => {
14 count++;
15 console.log(`[${count}] ${message}`);
16 }) as LoggerCounter;
17
18 // Add counter functionality
19 (fn as any).valueOf = () => count;
20 fn.reset = () => { count = 0; };
21
22 return fn;
23}
24
25// Usage
26const logCounter = createLoggerCounter();
27logCounter("First message"); // [1] First message
28logCounter("Second message"); // [2] Second message
29console.log(logCounter()); // 2
30logCounter.reset();Generic Intersection Types:
1// Generic intersections
2type WithMetadata<T> = T & {
3 metadata: {
4 createdAt: Date;
5 version: string;
6 tags: string[];
7 };
8};
9
10type WithValidation<T> = T & {
11 validate(): boolean;
12 errors: string[];
13};
14
15// Combining generic intersections
16type EnhancedModel<T> = WithMetadata<T> & WithValidation<T>;
17
18interface User {
19 id: string;
20 name: string;
21 email: string;
22}
23
24type EnhancedUser = EnhancedModel<User>;
25
26function createEnhancedUser(user: User): EnhancedUser {
27 return {
28 ...user,
29 metadata: {
30 createdAt: new Date(),
31 version: "1.0.0",
32 tags: ["user", "active"]
33 },
34 errors: [],
35 validate() {
36 this.errors = [];
37 if (!this.email.includes("@")) {
38 this.errors.push("Invalid email");
39 }
40 if (this.name.length < 2) {
41 this.errors.push("Name too short");
42 }
43 return this.errors.length === 0;
44 }
45 };
46}Conditional Intersection Types:
1// Conditional intersections based on generic constraints
2type AddTimestamp<T> = T extends { id: string }
3 ? T & { timestamp: Date }
4 : T;
5
6type AddValidation<T> = T extends object
7 ? T & { isValid: boolean }
8 : T;
9
10// Utility for enhancing objects
11type Enhance<T> = AddTimestamp<AddValidation<T>>;
12
13interface Product {
14 id: string;
15 name: string;
16 price: number;
17}
18
19interface Category {
20 name: string;
21 description: string;
22}
23
24type EnhancedProduct = Enhance<Product>;
25// Result: Product & { timestamp: Date } & { isValid: boolean }
26
27type EnhancedCategory = Enhance<Category>;
28// Result: Category & { isValid: boolean }
29
30function enhance<T extends object>(obj: T): Enhance<T> {
31 const enhanced = { ...obj, isValid: true } as any;
32
33 if ('id' in obj) {
34 enhanced.timestamp = new Date();
35 }
36
37 return enhanced;
38}Intersection with Union Types:
1// Complex combinations of intersections and unions
2interface BaseConfig {
3 host: string;
4 port: number;
5}
6
7interface DatabaseConfig {
8 type: "database";
9 connectionString: string;
10 poolSize: number;
11}
12
13interface CacheConfig {
14 type: "cache";
15 ttl: number;
16 maxSize: number;
17}
18
19interface LoggingConfig {
20 level: "debug" | "info" | "warn" | "error";
21 format: "json" | "text";
22}
23
24// Union of configs with shared base and logging
25type ServiceConfig = BaseConfig & LoggingConfig & (DatabaseConfig | CacheConfig);
26
27function validateConfig(config: ServiceConfig): boolean {
28 // All configs have BaseConfig and LoggingConfig properties
29 if (!config.host || config.port <= 0) return false;
30 if (!config.level || !config.format) return false;
31
32 // Type narrowing for union part
33 if (config.type === "database") {
34 return !!config.connectionString && config.poolSize > 0;
35 } else {
36 return config.ttl > 0 && config.maxSize > 0;
37 }
38}
39
40// Usage
41const dbConfig: ServiceConfig = {
42 host: "localhost",
43 port: 5432,
44 level: "info",
45 format: "json",
46 type: "database",
47 connectionString: "postgresql://...",
48 poolSize: 10
49};Type guards enable runtime type checking and type narrowing, allowing you to safely work with union types and unknown data.
Built-in Type Guards:
1// typeof type guard
2function processValue(value: unknown): string {
3 if (typeof value === "string") {
4 // TypeScript knows value is string
5 return value.toUpperCase();
6 }
7
8 if (typeof value === "number") {
9 // TypeScript knows value is number
10 return value.toFixed(2);
11 }
12
13 if (typeof value === "boolean") {
14 // TypeScript knows value is boolean
15 return value.toString();
16 }
17
18 if (typeof value === "object" && value !== null) {
19 // TypeScript knows value is object (but not null)
20 return JSON.stringify(value);
21 }
22
23 return "Unknown type";
24}
25
26// instanceof type guard
27class Dog {
28 bark(): void { console.log("Woof!"); }
29}
30
31class Cat {
32 meow(): void { console.log("Meow!"); }
33}
34
35function makeSound(animal: Dog | Cat): void {
36 if (animal instanceof Dog) {
37 animal.bark(); // TypeScript knows this is Dog
38 } else {
39 animal.meow(); // TypeScript knows this is Cat
40 }
41}
42
43// Array.isArray type guard
44function processData(data: string | string[]): string[] {
45 if (Array.isArray(data)) {
46 // TypeScript knows data is string[]
47 return data.map(item => item.toUpperCase());
48 }
49
50 // TypeScript knows data is string
51 return [data.toUpperCase()];
52}Custom Type Guards:
1// User-defined type guard functions
2interface User {
3 id: string;
4 name: string;
5 email: string;
6}
7
8interface Admin {
9 id: string;
10 name: string;
11 email: string;
12 permissions: string[];
13}
14
15// Type guard function
16function isAdmin(user: User | Admin): user is Admin {
17 return 'permissions' in user;
18}
19
20function handleUser(user: User | Admin): void {
21 console.log(`User: ${user.name}`);
22
23 if (isAdmin(user)) {
24 // TypeScript knows user is Admin
25 console.log(`Permissions: ${user.permissions.join(', ')}`);
26 }
27}
28
29// Type guard for API responses
30interface ApiSuccess<T> {
31 success: true;
32 data: T;
33}
34
35interface ApiError {
36 success: false;
37 error: string;
38 code: number;
39}
40
41type ApiResponse<T> = ApiSuccess<T> | ApiError;
42
43function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
44 return response.success === true;
45}
46
47async function handleApiResponse<T>(response: ApiResponse<T>): Promise<T> {
48 if (isApiSuccess(response)) {
49 // TypeScript knows response is ApiSuccess<T>
50 return response.data;
51 }
52
53 // TypeScript knows response is ApiError
54 throw new Error(`API Error (${response.code}): ${response.error}`);
55}Advanced Type Guards:
1// Generic type guards
2function isObject<T = any>(value: unknown): value is Record<string, T> {
3 return typeof value === "object" && value !== null && !Array.isArray(value);
4}
5
6function hasProperty<K extends string>(
7 obj: unknown,
8 key: K
9): obj is Record<K, unknown> {
10 return isObject(obj) && key in obj;
11}
12
13function hasStringProperty<K extends string>(
14 obj: unknown,
15 key: K
16): obj is Record<K, string> {
17 return hasProperty(obj, key) && typeof obj[key] === "string";
18}
19
20// Usage with unknown data
21function processUnknownData(data: unknown): void {
22 if (hasStringProperty(data, "name")) {
23 // TypeScript knows data has a string 'name' property
24 console.log(`Name: ${data.name}`);
25 }
26
27 if (hasProperty(data, "age") && typeof data.age === "number") {
28 console.log(`Age: ${data.age}`);
29 }
30}
31
32// Type guard for array of specific type
33function isArrayOf<T>(
34 value: unknown,
35 itemGuard: (item: unknown) => item is T
36): value is T[] {
37 return Array.isArray(value) && value.every(itemGuard);
38}
39
40function isString(value: unknown): value is string {
41 return typeof value === "string";
42}
43
44function isNumber(value: unknown): value is number {
45 return typeof value === "number";
46}
47
48// Usage
49const unknownArray: unknown = ["hello", "world", "typescript"];
50
51if (isArrayOf(unknownArray, isString)) {
52 // TypeScript knows unknownArray is string[]
53 console.log(unknownArray.map(s => s.toUpperCase()));
54}Assertion Functions:
1// Assertion functions throw if condition is not met
2function assertIsString(value: unknown): asserts value is string {
3 if (typeof value !== "string") {
4 throw new Error("Expected string");
5 }
6}
7
8function assertIsUser(value: unknown): asserts value is User {
9 if (!isObject(value)) {
10 throw new Error("Expected object");
11 }
12
13 if (!hasStringProperty(value, "id")) {
14 throw new Error("Expected id property");
15 }
16
17 if (!hasStringProperty(value, "name")) {
18 throw new Error("Expected name property");
19 }
20
21 if (!hasStringProperty(value, "email")) {
22 throw new Error("Expected email property");
23 }
24}
25
26// Usage
27function processUserData(data: unknown): void {
28 assertIsUser(data);
29 // After assertion, TypeScript knows data is User
30 console.log(`Processing user: ${data.name} (${data.email})`);
31}
32
33// Assertion function for non-null values
34function assertIsDefined<T>(value: T | null | undefined): asserts value is T {
35 if (value === null || value === undefined) {
36 throw new Error("Value is null or undefined");
37 }
38}
39
40function processUser(user: User | null): void {
41 assertIsDefined(user);
42 // TypeScript knows user is not null after assertion
43 console.log(user.name);
44}Type Guards with Discriminated Unions:
1// Advanced discriminated union with type guards
2interface LoadingState {
3 status: "loading";
4 progress?: number;
5}
6
7interface SuccessState {
8 status: "success";
9 data: any;
10 timestamp: Date;
11}
12
13interface ErrorState {
14 status: "error";
15 error: string;
16 retryCount: number;
17}
18
19type AsyncState = LoadingState | SuccessState | ErrorState;
20
21// Type guards for each state
22function isLoading(state: AsyncState): state is LoadingState {
23 return state.status === "loading";
24}
25
26function isSuccess(state: AsyncState): state is SuccessState {
27 return state.status === "success";
28}
29
30function isError(state: AsyncState): state is ErrorState {
31 return state.status === "error";
32}
33
34// Usage in component logic
35function renderAsyncState(state: AsyncState): string {
36 if (isLoading(state)) {
37 const progress = state.progress ? ` (${state.progress}%)` : "";
38 return `Loading${progress}...`;
39 }
40
41 if (isSuccess(state)) {
42 return `Success: Data loaded at ${state.timestamp.toISOString()}`;
43 }
44
45 if (isError(state)) {
46 return `Error: ${state.error} (retry count: ${state.retryCount})`;
47 }
48
49 // TypeScript ensures exhaustive checking
50 const exhaustiveCheck: never = state;
51 throw new Error(`Unhandled state: ${exhaustiveCheck}`);
52}Your progress is saved in this browser, on this device only.