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.
Installation
npx clawhub@latest install typescript-advancedView 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"]
}