Skip to main content
Coding Agents & IDEsDocumented

typescript-advanced

Advanced TypeScript type system. Conditional types, mapped types, template literals, discriminated unions, declaration merging, module augmentation, the satisfies operator, and tsconfig best practices for large codebases.

Share:

Installation

npx clawhub@latest install typescript-advanced

View the full skill documentation and source below.

Documentation

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"]
}