| name | typescript |
| description | Advanced TypeScript |
Boris Cherny TypeScript Principles
Applying Boris Cherny's TypeScript expertise from "Programming TypeScript" (O'Reilly) and his work on developer tools. Type safety is a feature, not a burden.
Core Philosophy
Let TypeScript Work For You
"The goal isn't to annotate everything—it's to annotate the minimum necessary and let TypeScript infer the rest."
TypeScript's power comes from its type inference. Over-annotating defeats the purpose.
const numbers: Array<number> = [1, 2, 3];
const doubled: Array<number> = numbers.map((n: number): number => n * 2);
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
Types as Documentation
Types should tell the story of what code does. If you need comments to explain types, the types aren't clear enough.
type T = { [K in keyof U]: U[K] extends F ? K : never }[keyof U];
type MethodNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type ArrayMethods = MethodNames<Array<unknown>>;
Type Inference Patterns
Const Assertions for Literal Types
const config = {
endpoint: '/api/users',
method: 'GET'
};
const config = {
endpoint: '/api/users',
method: 'GET'
} as const;
const actions = ['increment', 'decrement', 'reset'] as const;
type Action = typeof actions[number];
Inference from Usage
function createUser(name: string, age: number) {
return {
id: crypto.randomUUID(),
name,
age,
createdAt: new Date()
};
}
type User = ReturnType<typeof createUser>;
Generic Inference
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]);
first(['a', 'b']);
first([{ id: 1 }]);
Generic Patterns
Constrained Generics
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
getProperty(user, 'name');
getProperty(user, 'age');
getProperty(user, 'email');
Generic Defaults
type Container<T = unknown> = {
value: T;
timestamp: Date;
};
const box: Container = { value: 'anything', timestamp: new Date() };
const numberBox: Container<number> = { value: 42, timestamp: new Date() };
Generic Factories
function createStore<State extends object>(initialState: State) {
let state = initialState;
return {
getState: () => state,
setState: (newState: Partial<State>) => {
state = { ...state, ...newState };
},
subscribe: (listener: (state: State) => void) => {
}
};
}
const userStore = createStore({ name: '', loggedIn: false });
userStore.setState({ name: 'Alice' });
userStore.setState({ invalid: true });
Mapped Types
Transform Object Types
type Partial<T> = {
[K in keyof T]?: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
Key Remapping
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
type User = { name: string; age: number };
type PrefixedUser = Prefixed<User, 'user_'>;
type MethodsOnly<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
Conditional Types
Type-Level Branching
type IsString<T> = T extends string ? true : false;
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;
Distributive Conditionals
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
Infer Keyword
type Unpacked<T> =
T extends Array<infer U> ? U :
T extends Promise<infer U> ? U :
T extends (...args: any) => infer U ? U :
T;
type A = Unpacked<string[]>;
type B = Unpacked<Promise<number>>;
type C = Unpacked<() => boolean>;
type D = Unpacked<string>;
Discriminated Unions
Exhaustive Pattern Matching
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function area(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:
const _exhaustive: never = shape;
return _exhaustive;
}
}
Result Types
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: 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);
} else {
console.log(result.error);
}
Function Types
Overloads for Complex Signatures
function createElement(tag: 'a'): HTMLAnchorElement;
function createElement(tag: 'canvas'): HTMLCanvasElement;
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const anchor = createElement('a');
const canvas = createElement('canvas');
const div = createElement('div');
Type Guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new Error('Not a number');
}
}
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase());
}
assertIsNumber(input);
console.log(input.toFixed(2));
}
Module Patterns
Barrel Exports
export type { User, UserRole } from './user';
export type { Product, ProductCategory } from './product';
export type { Order, OrderStatus } from './order';
import type { User, Product, Order } from './types';
Type-Only Imports
import type { User } from './user';
import { createUser } from './user';
import { createUser, type User } from './user';
Best Practices
Prefer Interfaces for Objects
interface User {
id: string;
name: string;
}
interface Admin extends User {
permissions: string[];
}
type ID = string | number;
type Readonly<T> = { readonly [K in keyof T]: T[K] };
Avoid any, Embrace unknown
function parse(json: string): any {
return JSON.parse(json);
}
function parse(json: string): unknown {
return JSON.parse(json);
}
const data = parse('{"name": "Alice"}');
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name);
}
Use satisfies for Validation
const config = {
endpoint: '/api',
timeout: 5000,
retries: 3
} satisfies Record<string, string | number>;
config.endpoint;
config.timeout;
When to Apply
| Scenario | Apply Cherny |
|---|
| Designing type-safe APIs | Yes - generic patterns |
| Type-level programming | Yes - conditionals, mapped types |
| Reducing type annotations | Yes - leverage inference |
| Complex function signatures | Yes - overloads, guards |
| React component types | Partially - see react-state for React specifics |
| Build configuration | No - see reactivity |
Source Material
- "Programming TypeScript" (O'Reilly, 2019)
- TypeScript documentation contributions
- Conference talks on type-level programming
- Open source TypeScript projects