Advanced Types

TypeScript Intermediate — Advanced Types
TypeScript Intermediate
Course 2 · Chapter 1 · Advanced Types

🔷 Advanced Types

Now that you've mastered TypeScript fundamentals, it's time to level up. Advanced types — unions, intersections, type guards, conditionals, and mapped types — are the tools that separate basic type safety from true type mastery. These patterns unlock the full power of TypeScript's type system.

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.

→ Solution

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.

→ Solution

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.

→ Solution

⚠️ Gotcha: Overly Broad Unions

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.