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.