Advanced Types
๐ท Advanced Types
Union Types: Multiple Possibilities
A union type represents a value that could be one of several types. Use the | (pipe) operator:
type Status = "success" | "error" | "pending"; function handleResponse(status: Status) { if (status === "success") { console.log("All good!"); } } type ID = string | number; const userId: ID = "user-123"; // โ Both valid const postId: ID = 456; // โ Both valid
String Literals
Restrict to specific string values. Great for enums-like behavior without the syntax overhead.
Union of Types
Mix primitives: string | number, or objects with different shapes.
Intersection Types: Combine Constraints
An intersection type combines multiple types into one. Use the & operator:
type HasName = { name: string }; type HasAge = { age: number }; type Person = HasName & HasAge; const alice: Person = { name: "Alice", age: 30 }; // โ Must have both name and age
Union vs Intersection
Union (|)
Or: type can be one thing or another
type Result = string | number; // value is either string OR number
Intersection (&)
And: type must have properties from both
type Combined = A & B; // value must satisfy A AND B
Type Guards: Narrowing Union Types
When working with unions, TypeScript doesn't know which type you have. Type guards narrow the type so you can safely use type-specific operations:
type Input = string | number; function process(value: Input) { // Problem: TypeScript doesn't know if it's string or number // value.toUpperCase(); // โ Error: number doesn't have toUpperCase // Solution: Type guard with typeof if (typeof value === "string") { console.log(value.toUpperCase()); // โ Now safe } else { console.log(value + 10); // โ Safe arithmetic } }
typeof Guard
Check primitive types: string, number, boolean, undefined.
instanceof Guard
Check if a value is an instance of a class: obj instanceof Error.
Custom Guard
Write a function with return type value is Type for complex checks.
in Operator
Check if an object has a property: "email" in user.
Custom Type Guard Example
interface Dog { bark(): void; } interface Cat { meow(): void; } // Custom type guard function function isDog(pet: any): pet is Dog { return typeof pet.bark === "function"; } const myPet: Dog | Cat = getDog(); if (isDog(myPet)) { myPet.bark(); // โ TypeScript knows it's a Dog } else { myPet.meow(); // โ TypeScript knows it's a Cat }
Conditional Types: Types That Decide
Conditional types use the ternary operator to assign a type based on a condition:
type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<number>; // false // Practical example: get the return type of a function type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never; const add = (a: number, b: number) => a + b; type AddReturn = GetReturnType<typeof add>; // number
Key keyword: infer captures a type that will be inferred during type checking.
Mapped Types: Transform Types Systematically
Mapped types let you create new types by transforming properties of existing types:
interface User { id: number; name: string; email: string; } // Make all properties optional type UserPartial = { [K in keyof User]?: User[K]; }; // Result is equivalent to: // { // id?: number; // name?: string; // email?: string; // }
keyof
Gets all property names of a type as a union. keyof User โ "id" | "name" | "email"
in
Iterates over the union. Creates a new property for each.
[K]
Accesses the type of a property. User[K] gets the type of property K.
Utility Types
TypeScript provides built-in mapped types: Partial<T>, Readonly<T>, Record<K, V>
Practical Mapped Type: Read-Only Config
interface AppConfig { apiUrl: string; timeout: number; retries: number; } // Make all config properties readonly and add getters type ReadOnlyConfig = { readonly [K in keyof AppConfig]: AppConfig[K]; }; const config: ReadOnlyConfig = { apiUrl: "https://api.example.com", timeout: 5000, retries: 3 }; // config.timeout = 10000; // โ Error: readonly property
๐๏ธ Real-World Patterns
Pattern 1: Discriminated Unions (Tagged Unions)
Use a common property to distinguish between union members:
type Result<T> = | { status: "success"; data: T; } | { status: "error"; error: string; } function handle<T>(result: Result<T>) { if (result.status === "success") { console.log(result.data); // โ TypeScript knows data exists } else { console.log(result.error); // โ TypeScript knows error exists } }
Pattern 2: Extract Properties with Conditional Types
// Get only string properties from a type type StringPropertiesOnly<T> = { [K in keyof T]: T[K] extends string ? K : never; }[keyof T]; interface User { name: string; age: number; email: string; } type StringKeys = StringPropertiesOnly<User>; // "name" | "email"
๐ป Coding Challenges
Challenge 1: Union & Type Guard
Write a function that accepts either a User object or a user id (number). Inside, use a type guard to determine which was passed and handle each case differently.
Goal: Practice unions and type narrowing with typeof.
Challenge 2: Intersection & Custom Guard
Create two interfaces (e.g., Admin and Employee), then a type for a value that is both. Write a custom type guard function to check if something is an Admin.
Goal: Practice intersections and custom type predicates.
Challenge 3: Mapped Type
Take any interface and create a mapped type that makes all properties optional and readonly. Test it with a sample object.
Goal: Build comfort with keyof, in, and property transformations.
Avoid unions that are too broad โ they defeat the purpose of type safety. For example, string | number | boolean | object | any basically gives up on type checking. Use discriminated unions and type guards to narrow aggressively. The more specific your types, the more the compiler can help you.
๐ฏ What's Next
Now that you've mastered advanced types, we'll explore Generics Deep Dive โ how to write reusable, type-safe code that works across different types. This is where TypeScript truly shines.