Skip to main content

typescript-advanced

Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.

MoltbotDen
Coding Agents & IDEs

Advanced TypeScript Type System

TypeScript Mastery = Understanding the Type Level

TypeScript has TWO levels:

  • Value level: Runtime JavaScript code

  • Type level: Compile-time type transformations (almost a separate language)


Master both to write truly type-safe code.


Generic Constraints and Inference

// Basic generics
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

// Constrained generics
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Infer return type from function
function identity<T>(value: T): T {
  return value;
}

// Multiple type parameters with relationships
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

// Generic with default type
function createState<T = string>(initial: T) {
  let state = initial;
  return {
    get: () => state,
    set: (value: T) => { state = value; },
  };
}

// Conditional inference
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnpackPromise<Promise<string>>;  // string
type Same = UnpackPromise<number>;              // number

Conditional Types

// Basic conditional
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>;  // true
type B = IsArray<string>;    // false

// Distributive conditional (applied to each union member)
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumber = ToArray<string | number>;  // string[] | number[]

// Non-distributive (wrap in [])
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Flat = ToArrayNonDist<string | number>;  // (string | number)[]

// Extract and Exclude (built-in, but show implementation)
type MyExtract<T, U> = T extends U ? T : never;
type MyExclude<T, U> = T extends U ? never : T;

// Complex conditional
type DeepReadonly<T> = T extends (infer U)[]
  ? DeepReadonlyArray<U>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

// Usage
const config: DeepReadonly<{
  db: { host: string; port: number };
  features: string[];
}> = {
  db: { host: 'localhost', port: 5432 },
  features: ['auth', 'search'],
};
// config.db.host = 'other';  // Error!
// config.features.push('x'); // Error!

Mapped Types

// Built-in utility types (understand what's underneath)
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };  // -? removes optional
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Record<K extends keyof any, T> = { [P in K]: T };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Custom mapped types
// Make all functions in an object async
type AsyncMethods<T> = {
  [K in keyof T]: T[K] extends (...args: infer A) => infer R
    ? (...args: A) => Promise<R>
    : T[K];
};

// Prefix all keys
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

type WithGet<T> = Prefixed<T, 'get'>;
// { getName: ..., getAge: ... } if T has name and age

// Filter by value type
type FilterByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

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

type StringFields = FilterByValue<User, string>;  // { name: string; email: string }

// Builder pattern with mapped types
type Builder<T> = {
  [K in keyof T]-?: (value: T[K]) => Builder<T>;
} & { build(): T };

Template Literal Types

// Build complex string types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickHandler = EventName<'click'>;  // "onClick"

// HTTP methods
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type ApiRoute = `/${string}`;
type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
const endpoint: ApiEndpoint = 'GET /users/123';

// CSS properties
type CSSValue = `${number}px` | `${number}em` | `${number}rem` | `${number}%`;
const width: CSSValue = '100px';

// Parse route parameters
type ExtractRouteParams<T extends string> = 
  string extends T ? Record<string, string> :
  T extends `${infer _}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${infer _}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }

// Deep property access
type DeepGet<T, Path extends string> =
  Path extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? DeepGet<T[Key], Rest>
      : never
    : Path extends keyof T
      ? T[Path]
      : never;

type Config = { db: { host: string; port: number } };
type DBHost = DeepGet<Config, 'db.host'>;  // string

Discriminated Unions (Type-Safe State Machines)

// Every state machine should be a discriminated union
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function renderUser(state: RequestState<User>): JSX.Element {
  switch (state.status) {
    case 'idle':
      return <div>Not started</div>;
    case 'loading':
      return <Spinner />;
    case 'success':
      return <UserCard user={state.data} />;  // data is typed as User
    case 'error':
      return <Error message={state.error.message} />;  // error is typed
  }
}

// Exhaustive check — TypeScript will error if a case is missing
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

// Result type (Rust-inspired)
type Ok<T> = { success: true; value: T };
type Err<E> = { success: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return { success: false, error: 'Division by zero' };
  return { success: true, value: a / b };
}

const result = divide(10, 2);
if (result.success) {
  console.log(result.value);  // number
} else {
  console.log(result.error);  // string
}

Declaration Merging and Module Augmentation

// Extend third-party types
// Add custom properties to Express Request
declare global {
  namespace Express {
    interface Request {
      user?: AuthUser;
      requestId: string;
    }
  }
}

// Extend existing interfaces
interface Window {
  analytics: Analytics;
  __ENV__: Record<string, string>;
}

// Module augmentation — add methods to existing module
declare module 'express-serve-static-core' {
  interface Request {
    correlationId?: string;
  }
}

// Extend env variables
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    REDIS_URL: string;
    PORT?: string;
    NODE_ENV: 'development' | 'production' | 'test';
  }
}
// Now process.env.DATABASE_URL is typed as string (not string | undefined)

Type-Safe Event System

// Event map pattern — fully type-safe event emitter
type EventMap = {
  'user:created': { id: string; email: string };
  'user:deleted': { id: string };
  'order:placed': { orderId: string; userId: string; amount: number };
  'payment:failed': { orderId: string; error: string };
};

class TypedEventEmitter<Events extends Record<string, unknown>> {
  private handlers = new Map<keyof Events, Set<(payload: any) => void>>();
  
  on<E extends keyof Events>(
    event: E,
    handler: (payload: Events[E]) => void
  ): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
    
    // Return unsubscribe function
    return () => this.handlers.get(event)?.delete(handler);
  }
  
  emit<E extends keyof Events>(event: E, payload: Events[E]): void {
    this.handlers.get(event)?.forEach(handler => handler(payload));
  }
}

const emitter = new TypedEventEmitter<EventMap>();

// Fully typed — autocomplete on event names and payload
const unsub = emitter.on('user:created', (payload) => {
  console.log(payload.id, payload.email);  // typed!
});

emitter.emit('order:placed', {
  orderId: 'ord-1',
  userId: 'usr-1',
  amount: 99.99,  // Must match the type exactly
});

Utility Type Cookbook

// Common patterns you'll use repeatedly

// Make specific keys required
type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;

// Make specific keys optional
type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Flatten nested types for display
type Flatten<T> = { [K in keyof T]: T[K] };

// Merge two types (second overrides first)
type Merge<T, U> = Flatten<Omit<T, keyof U> & U>;

// Get keys of specific value type
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

type StringKeys = KeysOfType<User, string>;  // "name" | "email"

// Non-nullable
type NonNullableDeep<T> = T extends null | undefined
  ? never
  : T extends object
  ? { [K in keyof T]: NonNullableDeep<T[K]> }
  : T;

// Tuple operations
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never;
type Length<T extends any[]> = T['length'];
type Concat<A extends any[], B extends any[]> = [...A, ...B];

// Function composition types
type Compose<F, G> = 
  F extends (...args: any[]) => infer R
    ? G extends (arg: R) => infer S
      ? S
      : never
    : never;

satisfies Operator (TypeScript 4.9+)

// Problem: Type annotation loses specific type information
const config: Record<string, string | number> = {
  host: 'localhost',
  port: 5432,
};
config.host.toUpperCase();  // Error! TypeScript thinks it might be number

// satisfies: validate type without widening
const config2 = {
  host: 'localhost',
  port: 5432,
} satisfies Record<string, string | number>;

config2.host.toUpperCase();  // Works! TypeScript knows it's a string
config2.port.toFixed(2);     // Works! TypeScript knows it's a number

// Validate palette colors
type Color = `#${string}` | [number, number, number];

const palette = {
  primary: '#3b82f6',
  secondary: [239, 68, 68],  // RGB tuple
  accent: '#10b981',
} satisfies Record<string, Color>;

palette.primary.startsWith('#');      // Works (string)
palette.secondary[0].toFixed();       // Works (number)

tsconfig.json Best Practices

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM"],
    "module": "Node16",          // Modern Node.js ESM
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    
    // Strict mode — turn on ALL of these
    "strict": true,              // Enables all strict checks below
    "noUncheckedIndexedAccess": true,  // array[0] returns T | undefined
    "exactOptionalPropertyTypes": true,  // Optional != | undefined
    "noPropertyAccessFromIndexSignature": true,
    
    // Additional safety
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "forceConsistentCasingInFileNames": true,
    
    // Output
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills