Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Advanced Types

TypeScript’s type system is incredibly powerful — but more importantly, it is practical. Advanced types are not academic curiosities; they are the tools that let you model real business logic at the type level. Think of your basic types (string, number, boolean) as individual LEGO bricks. Advanced types are the techniques for combining, transforming, and constraining those bricks into complex structures that precisely describe your application’s data. A senior engineer would say: “If your types do not model your domain accurately, you are leaving bugs on the table that the compiler could have caught for free.”

1. Union Types (Revisited)

Union types represent values that can be one of several types. Think of a union as a labeled box that can hold different kinds of items — the box itself tells you “this contains either A or B,” and you must check which one before using it. Discriminated unions take this further by putting a tag on the item inside the box so you can identify it instantly.
// Basic union -- value can be either a string or a number, not both at once
type StringOrNumber = string | number;

// Discriminated unions (tagged unions) -- each variant has a "tag" field (success)
// that uniquely identifies which shape the object has.
// This pattern is the TypeScript equivalent of an algebraic data type.
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult<T>(result: Result<T>): void {
  if (result.success) {
    // TypeScript narrows: inside this branch, result MUST be { success: true; data: T }
    console.log(result.data);
  } else {
    // TypeScript narrows: inside this branch, result MUST be { success: false; error: string }
    console.log(result.error);
  }
}

Exhaustive Checks

This pattern is one of the most valuable defensive programming techniques in TypeScript. By assigning to never in the default branch, you create a compile-time tripwire: if someone adds a new shape variant (e.g., 'pentagon') but forgets to handle it in this switch, the compiler immediately flags the error.
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // Exhaustive check -- if all cases are handled, shape is 'never' here.
      // If you add a new variant to Shape and forget to handle it,
      // TypeScript will error: "Type 'pentagon' is not assignable to type 'never'"
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}
Practical tip: This pattern is critical in codebases where unions grow over time (think API response types, state machine states, or permission levels). Without it, adding a new variant silently falls through to the default case. A senior engineer would say: “Exhaustive checks turn runtime bugs into compile-time errors.”

---

## 2. Intersection Types (Revisited)

Intersection types combine multiple types into one. If a union (`|`) means "one or the other," an intersection (`&`) means "all of the above." Think of it as stacking transparencies on an overhead projector -- the result shows everything from every layer combined into a single image. The resulting type must satisfy *all* constituent types simultaneously.

```typescript
// Person describes one "layer" of properties
type Person = {
  name: string;
  age: number;
};

// Employee describes another "layer"
type Employee = {
  employeeId: string;
  department: string;
};

// Staff is the combination -- an object must have ALL properties from BOTH types
type Staff = Person & Employee;

const staff: Staff = {
  name: 'Alice',
  age: 30,
  employeeId: 'E001',
  department: 'Engineering'
  // Missing any of these four properties would cause a compile error
};

Intersection with Functions

type Logger = {
  log: (message: string) => void;
};

type Formatter = {
  format: (data: object) => string;
};

type LogFormatter = Logger & Formatter;

const logFormatter: LogFormatter = {
  log: (message) => console.log(message),
  format: (data) => JSON.stringify(data)
};

3. Conditional Types

Conditional types are the if/else of the type system. They let you create types that depend on conditions — “if T is a string, use type X; otherwise use type Y.” This is what makes TypeScript’s type system Turing-complete and enables the kind of type-level programming that powers libraries like Zod, tRPC, and Prisma.

Basic Syntax

// Reads as: "If T extends (is assignable to) string, then the type is true; otherwise false"
// This is a ternary operator, but at the type level instead of the value level.
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true -- string extends string
type B = IsString<number>;  // false -- number does not extend string
type C = IsString<'hello'>; // true -- 'hello' is a string literal, which extends string

Practical Examples

// Extract return type of a function -- "infer R" tells TypeScript:
// "I don't know what R is yet, but figure it out from the pattern match."
// This is how TypeScript's built-in ReturnType<T> utility works under the hood.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function greet(): string {
  return 'Hello';
}

type GreetReturn = ReturnType<typeof greet>; // string

// Flatten arrays -- if T is an array, extract the element type; otherwise keep T as-is
type Flatten<T> = T extends (infer U)[] ? U : T;

type StrArray = Flatten<string[]>; // string -- extracted from string[]
type Num = Flatten<number>;        // number -- not an array, so returned as-is
Practical tip: The infer keyword is like a “capture group” in a regex — it extracts a piece from a complex type. You will see it everywhere in library type definitions.

Distributive Conditional Types

When conditional types act on unions, they distribute over each member — meaning the condition is applied to each union member individually, then the results are combined back into a union. This is a common source of confusion, so pay close attention.
// TypeScript distributes: applies the condition to string AND number separately,
// then unions the results: string[] | number[]
type ToArray<T> = T extends any ? T[] : never;

type StrOrNumArray = ToArray<string | number>;
// Result: string[] | number[] (NOT (string | number)[])

// To prevent distribution, wrap both sides in a tuple.
// The tuple "shields" the union from being split apart.
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type Mixed = ToArrayNonDistributive<string | number>;
// Result: (string | number)[] -- the union stays intact
Pitfall: Distribution catches many developers off guard. If you write Exclude<'a' | 'b' | 'c', 'a'>, TypeScript checks each member against 'a' individually, yielding 'b' | 'c'. This is useful — but when you do not want it, the tuple wrapper trick is essential.

4. Mapped Types

Transform properties of existing types programmatically. Mapped types are the “find-and-replace” of the type system — they iterate over every property of a type and apply a transformation. This is how TypeScript’s built-in Partial<T>, Readonly<T>, Required<T>, and Pick<T, K> are implemented under the hood.

Basic Mapped Types

type User = {
  id: number;
  name: string;
  email: string;
};

// Make all properties optional
type PartialUser = {
  [K in keyof User]?: User[K];
};

// Make all properties readonly
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

// Make all properties nullable
type NullableUser = {
  [K in keyof User]: User[K] | null;
};

Mapped Type Modifiers

// Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove optional
type Required<T> = {
  [K in keyof T]-?: T[K];
};

// Add readonly and optional
type PartialReadonly<T> = {
  readonly [K in keyof T]?: T[K];
};

Key Remapping (TS 4.1+)

// Rename keys with template literals
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type User = {
  name: string;
  age: number;
};

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }

// Filter out certain keys
type RemoveKind<T> = {
  [K in keyof T as Exclude<K, 'kind'>]: T[K];
};

5. Template Literal Types

Create types from string patterns.
// Basic template literal
type Greeting = `Hello, ${string}`;
const g1: Greeting = 'Hello, World';  // ✅
// const g2: Greeting = 'Hi, World';  // ❌

// Combining unions
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Path = '/users' | '/products';
type Endpoint = `${HttpMethod} ${Path}`;
// 12 combinations: 'GET /users' | 'GET /products' | 'POST /users' | ...

// String manipulations
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'

Practical Example: CSS Units

type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = '100px';   // ✅
const height: CSSValue = '50vh';   // ✅
// const bad: CSSValue = '100';    // ❌

type CSSProperties = {
  width?: CSSValue;
  height?: CSSValue;
  margin?: CSSValue;
  padding?: CSSValue;
};

6. infer Keyword

Extract types from complex structures. The infer keyword is like a “capture group” in a regular expression — it matches a pattern and captures a piece of the type for you to use. It only works inside conditional types (T extends ... ? ... : ...) and is the key that unlocks most advanced type-level programming.
// "If T is a function, capture its return type as R and give it back to me"
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type FnReturn = GetReturnType<() => string>; // string

// Infer array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type El = ArrayElement<number[]>; // number

// Infer Promise resolved type
type Awaited<T> = T extends Promise<infer U> ? U : T;

type Resolved = Awaited<Promise<string>>; // string

// Infer function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type Params = Parameters<(a: string, b: number) => void>; // [string, number]

Advanced infer Patterns

// Extract first element of tuple
type First<T> = T extends [infer F, ...any[]] ? F : never;

type FirstEl = First<[string, number, boolean]>; // string

// Extract last element
type Last<T> = T extends [...any[], infer L] ? L : never;

type LastEl = Last<[string, number, boolean]>; // boolean

// Infer from string template
type ExtractPath<T> = T extends `/api/${infer Path}` ? Path : never;

type Path = ExtractPath<'/api/users'>; // 'users'

7. Built-in Utility Types

TypeScript provides many utility types out of the box. These are not special syntax — they are all implemented using mapped types, conditional types, and infer under the hood. Understanding them means you can read library type definitions and build your own when needed. Think of utility types as a standard toolbox: Partial is a wrench, Pick is a screwdriver, Omit is a saw — each shapes an existing type into what you need.

Object Utilities

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial<T> - All properties optional
type UpdateUser = Partial<User>;

// Required<T> - All properties required
type CompleteUser = Required<Partial<User>>;

// Readonly<T> - All properties readonly
type FrozenUser = Readonly<User>;

// Pick<T, K> - Select properties
type UserCredentials = Pick<User, 'email' | 'password'>;

// Omit<T, K> - Exclude properties
type PublicUser = Omit<User, 'password'>;

// Record<K, T> - Object with keys K and values T
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

Union Utilities

type Status = 'pending' | 'active' | 'inactive' | 'deleted';

// Exclude<T, U> - Remove types from union
type ActiveStatus = Exclude<Status, 'deleted'>; // 'pending' | 'active' | 'inactive'

// Extract<T, U> - Keep only matching types
type CommonStatus = Extract<Status, 'active' | 'unknown'>; // 'active'

// NonNullable<T> - Remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

Function Utilities

function fetchUser(id: number, options?: { cache: boolean }): Promise<User> {
  // ...
}

// Parameters<T> - Get function parameters as tuple
type FetchParams = Parameters<typeof fetchUser>; // [number, { cache: boolean }?]

// ReturnType<T> - Get function return type
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>

// ConstructorParameters<T> - Get constructor parameters
class User {
  constructor(public name: string, public age: number) {}
}
type UserParams = ConstructorParameters<typeof User>; // [string, number]

// InstanceType<T> - Get instance type of constructor
type UserInstance = InstanceType<typeof User>; // User

String Utilities

type Event = 'click' | 'focus' | 'blur';

type UpperEvent = Uppercase<Event>;     // 'CLICK' | 'FOCUS' | 'BLUR'
type LowerEvent = Lowercase<UpperEvent>; // 'click' | 'focus' | 'blur'
type CapEvent = Capitalize<Event>;      // 'Click' | 'Focus' | 'Blur'
type UncapEvent = Uncapitalize<CapEvent>; // 'click' | 'focus' | 'blur'

8. Custom Utility Types

Build your own utility types. Once you understand mapped types, conditional types, and infer, you can compose them into custom utilities tailored to your domain. These are the types that senior TypeScript developers write to make entire classes of bugs impossible. Each utility below is a real-world pattern you will encounter in production codebases.

Deep Partial

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
    name: string;
  };
}

const partialConfig: DeepPartial<Config> = {
  server: { host: 'localhost' }
  // port, database.url, database.name are all optional
};

DeepReadonly

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

const config: DeepReadonly<Config> = {
  server: { host: 'localhost', port: 3000 },
  database: { url: 'mongodb://...', name: 'mydb' }
};

// config.server.host = 'other'; // ❌ Error

Nullable

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type User = {
  name: string;
  email: string;
};

type NullableUser = Nullable<User>;
// { name: string | null; email: string | null; }

PickByType

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type StringProps = PickByType<User, string>;
// { name: string; email: string; }

type NumberProps = PickByType<User, number>;
// { id: number; age: number; }

OmitByType

type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

type NonStringProps = OmitByType<User, string>;
// { id: number; age: number; }

9. Type Challenges

Practice advanced types with these patterns. Type challenges are to TypeScript what algorithmic puzzles are to data structures — they sharpen your ability to think at the type level. The patterns below appear frequently in library source code and senior-level interviews.

Tuple to Union

type TupleToUnion<T extends readonly any[]> = T[number];

type Tuple = [string, number, boolean];
type Union = TupleToUnion<Tuple>; // string | number | boolean

Union to Intersection

type UnionToIntersection<U> = (
  U extends any ? (arg: U) => void : never
) extends (arg: infer I) => void
  ? I
  : never;

type Union = { a: string } | { b: number };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number }

Get Required Keys

type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

type User = {
  id: number;
  name: string;
  email?: string;
};

type Required = RequiredKeys<User>; // 'id' | 'name'

Get Optional Keys

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

type Optional = OptionalKeys<User>; // 'email'

10. Type-safe API Patterns

Type-safe Event Emitter

type EventMap = {
  login: { userId: string; timestamp: Date };
  logout: { userId: string };
  purchase: { productId: string; amount: number };
};

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners = new Map<keyof T, Set<(data: any) => void>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach((listener) => listener(data));
  }

  off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    this.listeners.get(event)?.delete(listener);
  }
}

const emitter = new TypedEventEmitter<EventMap>();

// Fully type-safe!
emitter.on('login', (data) => {
  console.log(data.userId);    // string
  console.log(data.timestamp); // Date
});

emitter.emit('login', { userId: '123', timestamp: new Date() });
// emitter.emit('login', { userId: 123 }); // ❌ Error

Type-safe Builder

type Builder<T, Built extends Partial<T> = {}> = {
  set<K extends keyof T>(
    key: K,
    value: T[K]
  ): Builder<T, Built & Pick<T, K>>;
  build(): Built extends T ? T : never;
};

// This pattern ensures all required properties are set before build()

Summary

ConceptExample
Uniontype A = string | number
Intersectiontype A = B & C
ConditionalT extends U ? X : Y
Mapped{ [K in keyof T]: T[K] }
Template Literaltype A = `on${string}`
inferT extends (infer U)[] ? U : never
PartialPartial<T>
RequiredRequired<T>
PickPick<T, 'a' | 'b'>
OmitOmit<T, 'a' | 'b'>
ReturnTypeReturnType<typeof fn>
ParametersParameters<typeof fn>
Next, we’ll take a deep dive into generics!

Interview Deep-Dive

Strong Answer:infer is TypeScript’s way of extracting a type from within a larger type structure. Think of it as pattern matching with a capture group.
  • The problem it solves: Without infer, you cannot decompose complex types. If you have a function type and want its return type, you would need the original definition. infer lets you extract it from any function type, even third-party ones.
  • How ReturnType works: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never. If T matches a function pattern, capture the return type as R. If not, give never.
  • Step-by-step: ReturnType<() => string> — does () => string match (...args: any[]) => infer R? Yes, R = string. Result: string. For ReturnType<number>: no match, result: never.
  • Beyond functions: type ArrayElement<T> = T extends (infer E)[] ? E : never extracts element types. type Awaited<T> = T extends Promise<infer U> ? U : T unwraps Promises.
infer only works inside the condition of a conditional type. It is TypeScript’s equivalent of destructuring for types.Follow-up: Can you nest ‘infer’ to extract from deeply nested structures?Yes. Use recursion: type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T. This unwraps Promise<Promise<string>> to string. TypeScript has a recursion depth limit (~100 levels) to prevent infinite loops.
Strong Answer:Distributive conditional types are one of TypeScript’s most confusing behaviors.
  • The rule: When a conditional type acts on a naked type parameter receiving a union, TypeScript distributes across each member individually, then combines results.
  • Example: type ToArray<T> = T extends any ? T[] : never. With T = string | number, result is string[] | number[], not (string | number)[].
  • The surprise: type IsString<T> = T extends string ? 'yes' : 'no' with IsString<string | number> gives 'yes' | 'no', not 'no', because distribution evaluates each member separately.
  • Disabling distribution: Wrap in a tuple: [T] extends [string] ? 'yes' : 'no'. Now the union is checked as a whole.
  • Why it exists: Powers utility types like Exclude<T, U> = T extends U ? never : T, which filters union members.
Follow-up: How does Exclude work internally?Exclude<'a' | 'b' | 'c', 'a'> distributes: 'a' matches, becomes never. 'b' and 'c' do not match, pass through. Result: 'b' | 'c'. The never is absorbed in the union.
Strong Answer:Mapped types iterate over keys of a type and transform each property — the type-level for...in loop.
  • Syntax: { [K in keyof T]: Transform<T[K]> } iterates over every key K and produces a transformed property.
  • Building DeepReadonly:
    • Shallow: { readonly [K in keyof T]: T[K] }. Only top-level properties become readonly.
    • Deep: type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] }. Recurse into nested objects.
    • Gotcha: Functions extend object, so exclude them: check T[K] extends Function first.
    • Another gotcha: Arrays need special handling for readonly arrays.
Partial, Required, Readonly, Pick, Omit are all mapped types in TypeScript’s standard library.Follow-up: What do ’+’ and ’-’ modifiers do?They add or remove property modifiers. -readonly removes readonly. -? removes optional (this is how Required<T> works internally). You can combine: { -readonly [K in keyof T]-?: T[K] } makes all properties mutable and required.
Strong Answer:This exercise tests generics, indexed access types, and constrained type parameters together.
  • Event map: interface AppEvents { login: { userId: string }; error: { message: string; code: number }; }. Keys are event names, values are payload types.
  • Generic class: class EventEmitter<T extends Record<string, any>>. The generic is the event map.
  • Type-safe on: on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void. Constrains event names to valid keys. T[K] is the payload type.
  • Type-safe emit: emit<K extends keyof T>(event: K, data: T[K]): void. Checks payload types at compile time.
  • Power: Adding an event is one line in the interface. All call sites are automatically type-checked.
Follow-up: How would you handle unsubscribe?Return a closure from on that removes the callback: on(...): () => void. The caller stores the unsubscribe function. This avoids reference equality bugs with anonymous callbacks that off(event, handler) would require.