TypeScript Handbook
TypeScript is a statically typed superset of JavaScript developed by Microsoft and first released in 2012. It compiles down to plain JavaScript and runs everywhere JS runs — browsers, Node.js, Deno, Bun, and edge runtimes. The type system is structural (duck-typed), not nominal, which makes it flexible enough to model complex JS patterns. TypeScript has become the de-facto standard for large-scale JavaScript projects: Angular, Next.js, Nuxt, Svelte, and Vue are all written in TypeScript.
Pick TypeScript when
- Any JavaScript project that will grow — TypeScript catches entire classes of runtime bugs (undefined property access, wrong argument types) at compile time, for free. The migration cost from JS is low; the payoff in large codebases is enormous.
- Team collaboration — types serve as living documentation. A new developer can understand an API contract by reading its types without running the code. IDEs give instant autocomplete and refactoring support.
- Building a library or SDK — publishing
.d.tsdeclaration files lets consumers of your library get full type checking without reading source. The ecosystem expects types now. - Complex domain modelling — discriminated unions, mapped types, conditional types, and template literal types let you encode business rules in the type system and have the compiler enforce them.
- React, Vue, or Angular frontends — all three frameworks have first-class TypeScript support. Props types, store types, and route types eliminate entire categories of UI bugs.
Think twice before choosing TypeScript when
- One-off scripts — if you're writing a 20-line script that runs once, the tsconfig setup and compilation step is overhead. Plain JS or Python is faster to reach for.
- You need fully sound types — TypeScript's type system has intentional unsoundness (type assertions,
any, structural subtyping of functions). For provably correct programs, use a language with a sound type system like Haskell, Elm, or Rust. - Build pipeline complexity is a concern — TypeScript requires a compilation step. In very simple setups (plain HTML + a CDN script tag), JS is easier.
TypeScript vs. its closest alternatives
- TypeScript vs JavaScript — TypeScript is a strict upgrade for any project beyond a trivial script. The cost is near zero (especially with
allowJsfor gradual migration); the safety gains are large. - TypeScript vs Flow — Facebook's Flow is a similar typed JS project but has largely lost the ecosystem battle. TypeScript has better tooling, broader adoption, and more active development.
- TypeScript vs Dart — Dart (Flutter) compiles to native mobile/desktop apps and has a sound type system. TypeScript is better for web; Dart is better for cross-platform native apps.
- TypeScript vs Go/Rust for backend — TypeScript on Node is excellent for I/O-bound services and where JS ecosystem libraries are needed. Go and Rust win on raw throughput, binary size, and memory footprint for CPU-bound or high-concurrency services.
Resources
- typescriptlang.org — official TypeScript website
- TypeScript Handbook — the official language documentation
- TypeScript Playground — run TS in the browser with full type checking
- Total TypeScript — advanced courses and free tutorials by Matt Pocock
- Type Challenges — collection of TypeScript type system exercises
Topics
Primitives & Literals
// Primitive types
let name: string = 'Alice';
let age: number = 30;
let active: boolean = true;
let nothing: null = null;
let undef: undefined = undefined;
let id: symbol = Symbol('id');
let big: bigint = 100n;
// any (opt-out of type checking)
let loose: any = 'hello';
loose = 42; // OK
// unknown (safer any — must narrow before use)
let input: unknown = getData();
if (typeof input === 'string') {
input.toUpperCase(); // safe inside guard
}
// never (unreachable / always throws)
function fail(msg: string): never {
throw new Error(msg);
}
// Literal types
let direction: 'left' | 'right' | 'up' | 'down' = 'left';
let status: 200 | 400 | 404 | 500 = 200;
const PI = 3.14; // type is 3.14, not number
// Arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ['a', 'b'];
// Tuple
let pair: [string, number] = ['Alice', 30];
// Readonly
const readOnly: readonly number[] = [1, 2, 3];Unions & Intersections
// Union
type StringOrNumber = string | number;
function format(val: string | number): string {
return String(val);
}
// Intersection
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const p: Person = { name: 'Alice', age: 30 };
// Discriminated union (tagged union)
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'rectangle': return s.width * s.height;
}
}Interfaces & Types
// Interface
interface User {
readonly id: number;
name: string;
email?: string; // optional
greet(): string;
}
// Extending interface
interface Admin extends User {
permissions: string[];
}
// Type alias
type Point = { x: number; y: number };
type ID = string | number;
// Index signature
interface StringMap {
[key: string]: string;
}
// Generic interface
interface Repository<T> {
findById(id: number): Promise<T>;
save(entity: T): Promise<T>;
delete(id: number): Promise<void>;
}Generics
// Generic function
function identity<T>(value: T): T {
return value;
}
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// Constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Generic class
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
get size() { return this.items.length; }
}
const stack = new Stack<number>();
// Default type parameter
type ApiResponse<T = unknown> = {
data: T;
status: number;
error?: string;
};Functions
// Typed function
function add(a: number, b: number): number {
return a + b;
}
// Optional & default
function greet(name: string, greeting?: string): string {
return `${greeting ?? 'Hello'}, ${name}!`;
}
// Overloads
function format(value: string): string;
function format(value: number, decimals: number): string;
function format(value: string | number, decimals = 2): string {
if (typeof value === 'string') return value.trim();
return value.toFixed(decimals);
}
// Function type
type Predicate<T> = (value: T) => boolean;
const isPositive: Predicate<number> = n => n > 0;
// void and never
function log(msg: string): void { console.log(msg); }
function throwError(msg: string): never { throw new Error(msg); }Classes
class Animal {
readonly id: number;
name: string;
#sound: string; // private class field
constructor(name: string, sound: string) {
this.id = Math.random();
this.name = name;
this.#sound = sound;
}
speak(): string {
return `${this.name} says ${this.#sound}`;
}
protected info(): string {
return `Animal: ${this.name}`;
}
}
// Shorthand constructor parameters
class User {
constructor(
public name: string,
private email: string,
readonly createdAt: Date = new Date()
) {}
}
// Abstract class
abstract class Shape {
abstract area(): number;
toString() { return `Shape with area ${this.area()}`; }
}Utility Types
interface User {
id: number;
name: string;
email: string;
age?: number;
}
type UserDraft = Partial<User>; // all optional
type FullUser = Required<User>; // all required
type ImmutableUser = Readonly<User>; // all readonly
type UserPreview = Pick<User, 'id' | 'name'>;
type UserWithoutId = Omit<User, 'id'>;
type PageMap = Record<string, User[]>;
// Union manipulation
type NoNull = Exclude<string | number | null, null>;
type Strings = Extract<string | number | boolean, string>;
type Safe = NonNullable<string | null | undefined>; // string
// Function types
function getUser() { return { id: 1, name: 'Alice' }; }
type UserReturn = ReturnType<typeof getUser>;
type AddParams = Parameters<typeof add>; // [number, number]
type UserData = Awaited<Promise<User>>;
// Template literal utility
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`; // 'onClick' | 'onFocus' | 'onBlur'Type Narrowing
// typeof
function process(val: string | number) {
if (typeof val === 'string') {
return val.toUpperCase();
}
return val.toFixed(2);
}
// instanceof
function handleError(err: unknown) {
if (err instanceof Error) {
console.log(err.message);
}
}
// in operator
type Cat = { meow(): void };
type Dog = { bark(): void };
function speak(animal: Cat | Dog) {
if ('meow' in animal) animal.meow();
else animal.bark();
}
// Type predicate
function isString(val: unknown): val is string {
return typeof val === 'string';
}
// Exhaustiveness check
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${x}`);
}Decorators
// Enable: 'experimentalDecorators': true in tsconfig
// Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = 'report';
constructor(public title: string) {}
}
// Method decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key}`);
return original.apply(this, args);
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number) { return a + b; }
}tsconfig
{
\"compilerOptions\": {
\"target\": \"ES2022\",
\"module\": \"ESNext\",
\"moduleResolution\": \"bundler\",
\"lib\": [\"ES2022\", \"DOM\"],
\"outDir\": \"./dist\",
\"rootDir\": \"./src\",
\"strict\": true,
\"noUncheckedIndexedAccess\": true,
\"noImplicitOverride\": true,
\"exactOptionalPropertyTypes\": true,
\"skipLibCheck\": true,
\"declaration\": true,
\"sourceMap\": true,
\"paths\": {
\"@/*\": [\"./src/*\"]
}
},
\"include\": [\"src\"],
\"exclude\": [\"node_modules\", \"dist\"]
}Mapped Types
// Basic mapped type — iterate over all keys of T
type ReadonlyT<T> = { readonly [K in keyof T]: T[K] };
type OptionalT<T> = { [K in keyof T]?: T[K] };
type NullableT<T> = { [K in keyof T]: T[K] | null };
// Remapping keys with 'as' (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// { getName: () => string; getAge: () => number; ... }
// Filtering keys with never
type OmitFunctions<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
// +readonly / -readonly | +? / -?
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type AllRequired<T> = { [K in keyof T]-?: T[K] };
// DeepReadonly<T> — recursive mapped type
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// Literal union as key source
type Flags = { [K in 'a' | 'b' | 'c']: number };
// { a: number; b: number; c: number }Conditional Types
// Basic: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// Distributive conditional types — distributes over union members
type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>; // string[] | number[]
// NonNullable reimplemented
type MyNonNullable<T> = T extends null | undefined ? never : T;
type D = MyNonNullable<string | null | undefined>; // string
// Flatten<T> — unwrap one level of Array
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type E = Flatten<string[]>; // string
type F = Flatten<number>; // number
// UnwrapPromise<T>
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type G = UnwrapPromise<Promise<string>>; // string
type H = UnwrapPromise<boolean>; // boolean
// Conditional return type in a generic function signature
declare function process<T>(val: T): T extends string ? string[] : number;
// Prevent distribution with a tuple wrapper
type NoDistribute<T> = [T] extends [string] ? true : false;
type I = NoDistribute<string | number>; // false (not distributed)infer & Type Inference
// ReturnType reimplemented with infer
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet() { return 'hello'; }
type A = MyReturnType<typeof greet>; // string
// Parameters reimplemented with infer
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
function add(a: number, b: number) { return a + b; }
type B = MyParameters<typeof add>; // [number, number]
// InstanceType
type MyInstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;
class User { name = 'Alice'; }
type C = MyInstanceType<typeof User>; // User
// UnpackArray<T>
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type D = UnpackArray<string[]>; // string
type E = UnpackArray<number>; // number
// Inferring from nested positions
type UnpackPromiseArray<T> = T extends Promise<(infer U)[]> ? U : never;
type F = UnpackPromiseArray<Promise<string[]>>; // string
// Multiple infers in one conditional
type SwapArgs<T> = T extends (a: infer A, b: infer B) => infer R
? (a: B, b: A) => R
: never;
declare function swap(a: number, b: string): boolean;
type G = SwapArgs<typeof swap>; // (a: string, b: number) => booleanTemplate Literal Types
// Basic template literal type
type Greeting = `Hello, ${string}`;
// Combining unions — cartesian product of all combinations
type Color = 'red' | 'blue';
type Size = 'sm' | 'lg';
type Class = `${Color}-${Size}`;
// 'red-sm' | 'red-lg' | 'blue-sm' | 'blue-lg'
// Built-in intrinsic string manipulation types
type U = Uppercase<'hello'>; // 'HELLO'
type L = Lowercase<'HELLO'>; // 'hello'
type C = Capitalize<'hello'>; // 'Hello'
type N = Uncapitalize<'Hello'>; // 'hello'
// CamelToSnake via recursive template literal + infer
type CamelToSnake<S extends string> =
S extends `${infer H}${infer T}`
? H extends Uppercase<H>
? `_${Lowercase<H>}${CamelToSnake<T>}`
: `${H}${CamelToSnake<T>}`
: S;
type Snake = CamelToSnake<'helloWorld'>; // 'hello_world'
// Event handler name generation
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// PathParams extraction
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<'/users/:id/posts/:postId'>;
// 'id' | 'postId'Modules & Namespaces
// ES module imports / exports
import { helper } from './utils';
import defaultExport from './module';
export { helper };
export default function main() {}
// import type / export type — erased at compile time, zero runtime cost
import type { User } from './types';
export type { User };
// Namespace (legacy — prefer ES modules for new code)
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) { return /^[A-Za-z]+$/.test(s); }
}
}
// Module augmentation — extend types from an external package
declare module 'express' {
interface Request {
user?: { id: number; name: string };
}
}
// Ambient declarations (typically in a .d.ts file)
declare const __VERSION__: string;
declare function fetcher(url: string): Promise<Response>;
// declare global — extend global scope from a module file
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
// Triple-slash reference directive
/// <reference types='node' />
// paths alias — configured in tsconfig compilerOptions.paths
// import { x } from '@/utils'; resolves to ./src/utilsAdvanced Patterns
// Builder pattern — fluent interface via method chaining
class QueryBuilder {
private filters: string[] = [];
private sortField = '';
where(condition: string): this { this.filters.push(condition); return this; }
orderBy(field: string): this { this.sortField = field; return this; }
limit(n: number): this { return this; }
build(): string { return this.filters.join(' AND '); }
}
const q = new QueryBuilder().where('active = true').orderBy('name').limit(10).build();
// Branded / opaque types — prevent mixing structurally-identical primitives
type Opaque<T, Brand> = T & { readonly __brand: Brand };
type UserId = Opaque<number, 'UserId'>;
type OrderId = Opaque<number, 'OrderId'>;
const uid = 1 as UserId;
// getUser(uid) ok | getUser(1 as OrderId) type error
// Exhaustive switch with assertNever
type Status = 'pending' | 'active' | 'closed';
function assertNever(x: never): never { throw new Error(`Unhandled: ${x}`); }
function handle(s: Status): string {
switch (s) {
case 'pending': return 'Waiting';
case 'active': return 'Running';
case 'closed': return 'Done';
default: return assertNever(s);
}
}
// satisfies operator (TS 4.9) — validate shape without widening
const palette = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Record<string, string | number[]>;
palette.red; // number[], not string | number[]
// as const — narrow to literal types
const config = { env: 'production', port: 3000 } as const;
// config.env is 'production', not string
// infer in template literals — extract string sub-parts
type GetParam<T extends string> =
T extends `${string}:${infer P}` ? P : never;
type R = GetParam<'/user/:id'>; // 'id'Variance
TypeScript is structurally typed — variance emerges from how types are used, not declared. Return types are covariant (you can return a subtype), parameter types are contravariant (you must accept a supertype). The in/out modifiers (TS 4.7) make variance explicit and improve type-checking performance.
// Covariance — T can be substituted with a subtype (output positions)
// Array<T> is covariant in T (TypeScript uses structural typing)
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // OK — Dog extends Animal
// Function return types are covariant
type MakeDog = () => Dog;
type MakeAnimal = () => Animal;
const f: MakeAnimal = (): Dog => new Dog(); // OK — covariant return
// Function parameter types are contravariant (opposite direction)
type EatDog = (d: Dog) => void;
type EatAnimal = (a: Animal) => void;
const g: EatDog = (a: Animal) => {}; // OK — contravariant param
// Practical: callbacks
type Handler<T> = (event: T) => void;
// If MouseEvent extends Event:
declare const onMouse: Handler<MouseEvent>;
const onEvent: Handler<Event> = onMouse; // Error — MouseEvent is narrower
// Bivariant method shorthand vs. strict function property (--strictFunctionTypes)
interface Container<T> {
// method shorthand — bivariant (loose)
transform(x: T): T;
// property function — contravariant in param, covariant in return (strict)
process: (x: T) => T;
}
// in/out annotations (TS 4.7) — explicit variance for better perf and clarity
type Provider<out T> = () => T; // covariant — T only produced
type Consumer<in T> = (x: T) => void; // contravariant — T only consumedDeclaration Files
Declaration files (.d.ts) are TypeScript's API description format — pure type information with no runtime code. They let you add types to untyped JavaScript libraries, extend third-party module interfaces, and declare ambient globals.
// Declaration files (.d.ts) describe the shape of JS modules for TypeScript.
// You never ship .d.ts to users — TypeScript uses them at compile time only.
// ── Ambient variable declarations ─────────────────────────────────────────
declare const __VERSION__: string;
declare function fetcher(url: string): Promise<Response>;
declare class EventEmitter {
on(event: string, cb: (...args: unknown[]) => void): this;
emit(event: string, ...args: unknown[]): boolean;
}
// ── Module declaration — typed stub for an untyped JS package ─────────────
// file: types/some-legacy-lib.d.ts
declare module 'some-legacy-lib' {
export interface Options { timeout?: number; retries?: number; }
export function fetch(url: string, opts?: Options): Promise<string>;
export const version: string;
export default function init(): void;
}
// ── Global augmentation — extend the global scope ─────────────────────────
declare global {
interface Window {
analytics: { track(event: string, props?: object): void };
}
interface Array<T> {
last(): T | undefined;
}
}
// ── Module augmentation — extend a third-party module ─────────────────────
import 'express';
declare module 'express' {
interface Request {
user?: { id: number; email: string };
requestId: string;
}
}
// ── Triple-slash directives ────────────────────────────────────────────────
/// <reference types='node' /> // pulls in @types/node globals
/// <reference path='./extra.d.ts' /> // includes another .d.ts file
/// <reference lib='dom' /> // enables DOM lib without tsconfig changeRecursive Types
Recursive type aliases reference themselves to model JSON, tree structures, deeply-nested configurations, and path extraction. TypeScript limits recursion depth to prevent infinite loops during compilation — use interface for recursive object types when possible, as they resolve lazily.
// JSON — a value can be any JSON type, including nested
type Json =
| null
| boolean
| number
| string
| Json[]
| { [key: string]: Json };
const data: Json = { name: 'Alice', scores: [1, 2, [3, 4]], active: true };
// Recursive tree
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
// Deep partial — recursively make all properties optional
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// Deep readonly — recursively make all properties readonly
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// Flatten nested array one level
type Flatten<T> = T extends (infer U)[] ? U : T;
// Path extraction — all dot-separated key paths in an object
// (uses conditional string concat to avoid nested template literals)
type Dot<D extends string, K extends string> =
D extends '' ? K : `${D}.${K}`;
type Paths<T, D extends string = ''> = {
[K in keyof T & string]:
T[K] extends object
? Paths<T[K], Dot<D, K>>
: Dot<D, K>;
}[keyof T & string];
type Config = { db: { host: string; port: number }; debug: boolean };
type ConfigPaths = Paths<Config>; // 'db.host' | 'db.port' | 'debug'Assertion Functions
An assertion function has the return type asserts condition or asserts x is T. After a successful call TypeScript narrows the type in the subsequent code, exactly like a type guard — but by throwing on failure rather than returning false.
// Assertion function — throws if condition is false; narrows type afterward
function assert(condition: unknown, msg: string): asserts condition {
if (!condition) throw new Error(msg);
}
function assertDefined<T>(val: T, msg = 'Expected defined value'): asserts val is NonNullable<T> {
if (val === null || val === undefined) throw new Error(msg);
}
// Usage — TypeScript narrows after the assertion call
function process(val: string | null) {
assertDefined(val, 'val must not be null');
val.toUpperCase(); // OK — val is string here
}
// Assertion function as a type predicate
function isString(val: unknown): asserts val is string {
if (typeof val !== 'string') throw new TypeError(`Expected string, got ${typeof val}`);
}
// Use in tests (e.g. Vitest / Jest)
function assertDeepEqual<T>(a: T, b: T): asserts a is T {
if (JSON.stringify(a) !== JSON.stringify(b))
throw new Error(`Expected ${JSON.stringify(a)} to equal ${JSON.stringify(b)}`);
}
// assertNever — exhaustiveness guard
type Status = 'open' | 'closed' | 'pending';
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
function describeStatus(s: Status): string {
switch (s) {
case 'open': return 'Open';
case 'closed': return 'Closed';
case 'pending': return 'Pending';
default: return assertNever(s); // type error if a case is missing
}
}const Type Parameters
The const modifier on a type parameter (TS 5.0) infers the literal type of an argument without requiring as const at the call site. The satisfies operator (TS 4.9) validates that a value matches a type while preserving the most specific inferred type.
// const type parameters (TS 5.0) — infer literal types without 'as const'
function identity<const T>(x: T): T { return x; }
identity([1, 2, 3]); // type is readonly [1, 2, 3], not number[]
identity({ a: 1, b: 2 }); // type is { readonly a: 1; readonly b: 2 }
// Compare without const — types are widened
function widened<T>(x: T): T { return x; }
widened([1, 2, 3]); // type is number[]
widened({ a: 1 }); // type is { a: number }
// Practical: route builder with literal path inference
function route<const Path extends string>(path: Path) {
return { path, match: (s: string) => s === path };
}
const r = route('/users/:id');
r.path; // type is '/users/:id', not string
// satisfies operator (TS 4.9) — validate type without widening
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;
palette.red; // type is number[], not string | number[]
palette.green; // type is string, not string | number[]
// using declaration (TS 5.2) — Symbol.dispose / Symbol.asyncDispose
class DbConnection {
[Symbol.dispose]() { console.log('closing connection'); }
}
function query() {
using conn = new DbConnection();
// conn is automatically disposed when the block exits
return 'results';
}keyof, typeof & Indexed Access
keyof T extracts a union of all keys of a type, typeof x captures the compile-time type of a value, and indexed access types (T[K]) let you look up the type of any property. Together they form the backbone of type-safe property access, array element inference, and mapped-type key remapping.
// keyof — produces a union of all keys of a type
type User = { id: number; name: string; email: string };
type UserKey = keyof User; // 'id' | 'name' | 'email'
// typeof — capture the type of a value
const config = { host: 'localhost', port: 5432 } as const;
type Config = typeof config;
// { readonly host: 'localhost'; readonly port: 5432 }
// Indexed access types — T[K] looks up a property type
type IdType = User['id']; // number
type NameType = User['name']; // string
type AnyValue = User[keyof User]; // number | string
// Combine keyof + generics for type-safe property access
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const nameVal = getField({ id: 1, name: 'Alice' }, 'name'); // string
// Array element type
const roles = ['admin', 'editor', 'viewer'] as const;
type Role = typeof roles[number]; // 'admin' | 'editor' | 'viewer'
// Nested indexed access
type DeepProp = {
user: { address: { city: string; zip: number } };
};
type CityType = DeepProp['user']['address']['city']; // string
// keyof with mapped types — transforms each key
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Booleans<T> = { [K in keyof T]: boolean };
// Narrowing with keyof
function hasKey<T extends object>(obj: T, key: PropertyKey): key is keyof T {
return key in obj;
}
// ReturnType via indexed access
interface Actions {
fetch(): Promise<User[]>;
save(u: User): Promise<void>;
}
type FetchReturn = ReturnType<Actions['fetch']>; // Promise<User[]>Function Overloads & this
TypeScript supports multiple call signatures for the same function or method, allowing the return type to depend on the argument types. The explicit this parameter enforces the receiver type inside methods and callbacks, while polymorphic this preserves the concrete subclass type through fluent builder chains.
// Function overloads — multiple signatures, one implementation
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {
if (typeof input === 'string') return parseInt(input, 10);
return input.toString();
}
const a = parse('42'); // number
const b = parse(99); // string
// Method overloads on a class
class Formatter {
format(value: string): string;
format(value: number, decimals: number): string;
format(value: string | number, decimals = 2): string {
if (typeof value === 'string') return value.trim();
return value.toFixed(decimals);
}
}
// this parameter — explicit type for the receiver
interface Counter {
count: number;
increment(this: Counter): Counter;
}
const counter: Counter = {
count: 0,
increment() { this.count++; return this; },
};
// this in callbacks — prevent accidental detachment
class Button {
label = 'Click me';
handleClick(this: Button): void {
console.log(this.label); // 'this' is always Button
}
}
// Polymorphic this — enables fluent interfaces on subclasses
class Builder {
protected config: Record<string, unknown> = {};
set(key: string, value: unknown): this { this.config[key] = value; return this; }
}
class AdvancedBuilder extends Builder {
validate(): this { return this; }
}
const result = new AdvancedBuilder().set('a', 1).validate().set('b', 2);
// Call signatures on object types
interface Multiplier {
(factor: number): number;
base: number;
}
const triple = Object.assign((f: number) => 3 * f, { base: 3 }) as Multiplier;Error Types & Result Pattern
Typed error hierarchies replace stringly-typed error codes with structured, narrowable classes. The Result / Either pattern wraps fallible operations in a discriminated union so callers must handle both success and failure paths without try/catch, making error flows explicit in the type system.
// Typed error hierarchy
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode = 500
) {
super(message);
this.name = 'AppError';
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string | number) {
super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(
public readonly field: string,
message: string
) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
}
}
// Result / Either pattern — no-throw error handling
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = AppError> = Ok<T> | Err<E>;
function ok<T>(value: T): Ok<T> { return { ok: true, value }; }
function err<E>(error: E): Err<E> { return { ok: false, error }; }
async function findUser(id: number): Promise<Result<{ id: number; name: string }>> {
if (id <= 0) return err(new ValidationError('id', 'id must be positive'));
const user = await fetch(`/api/users/${id}`).then(r => r.json()).catch(() => null);
if (!user) return err(new NotFoundError('User', id));
return ok(user);
}
// Consuming a Result
const res = await findUser(1);
if (res.ok) {
console.log(res.value.name);
} else {
if (res.error instanceof NotFoundError) {
console.error('404:', res.error.message);
} else {
console.error('Error:', res.error.code);
}
}
// catch type — always unknown in strict mode
try {
JSON.parse('bad json');
} catch (e: unknown) {
if (e instanceof SyntaxError) console.error(e.message);
}Advanced Generic Patterns
Variadic tuple types enable type-safe argument spreading and concatenation. Curried generic functions carry full type information across partial applications. Phantom (tagged) types prevent mixing structurally identical values, and recursive conditionals extract deep leaf types — all without any runtime overhead.
// Variadic tuple types (TS 4.0+) — spread in tuple positions
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
type Pair = Concat<[string], [number]>; // [string, number]
function concat<A extends unknown[], B extends unknown[]>(a: A, b: B): [...A, ...B] {
return [...a, ...b];
}
const combined = concat([1, 'hello'] as const, [true] as const); // [1, 'hello', true]
// Partial application at the type level
type Curry<F> = F extends (a: infer A, b: infer B) => infer R
? (a: A) => (b: B) => R
: never;
function curry<A, B, R>(f: (a: A, b: B) => R): (a: A) => (b: B) => R {
return a => b => f(a, b);
}
const add = curry((a: number, b: number) => a + b);
const add5 = add(5); // (b: number) => number
add5(3); // 8
// Recursive conditional — collect leaf types
type Leaves<T> = T extends object
? { [K in keyof T]: Leaves<T[K]> }[keyof T]
: T;
type Flat = Leaves<{ a: { b: string; c: number }; d: boolean }>; // string | number | boolean
// Phantom type — attach a type tag without runtime overhead
declare const __brand: unique symbol;
type Tagged<T, Tag extends string> = T & { readonly [__brand]: Tag };
type MetersT = Tagged<number, 'Meters'>;
type SecondsT = Tagged<number, 'Seconds'>;
const dist = 100 as MetersT;
const time = 10 as SecondsT;
// const bad: MetersT = time; // Error — different tags
// Generic pipeline — compose transformations with inferred types
function pipe<A>(a: A): A;
function pipe<A, B>(a: A, f1: (a: A) => B): B;
function pipe<A, B, C>(a: A, f1: (a: A) => B, f2: (b: B) => C): C;
function pipe(value: unknown, ...fns: Array<(v: unknown) => unknown>): unknown {
return fns.reduce((v, f) => f(v), value);
}
const out = pipe(' hello ', s => (s as string).trim(), s => (s as string).toUpperCase());Advanced Type Guards
Beyond simple typeof and instanceof, TypeScript supports structural duck-typing predicates, composable guard combinators, and array-of-T guards. Coupling these with discriminated unions and assertNever achieves fully exhaustive, type-safe branching across complex domain models.
// Discriminated union guard with exhaustiveness
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; w: number; h: number }
| { kind: 'triangle'; base: number; height: number };
function assertNever(x: never): never { throw new Error(`Unhandled: ${JSON.stringify(x)}`); }
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'rect': return s.w * s.h;
case 'triangle': return (s.base * s.height) / 2;
default: return assertNever(s);
}
}
// Structural predicate — duck-typing check
function hasFields<T extends object>(
val: unknown,
...keys: (keyof T)[]
): val is T {
return typeof val === 'object' && val !== null &&
keys.every(k => k in (val as object));
}
interface ApiUser { id: number; name: string; email: string }
const data: unknown = JSON.parse('{}');
if (hasFields<ApiUser>(data, 'id', 'name', 'email')) {
data.name.toUpperCase(); // safe
}
// Assertion-style guard that throws
function assertShape<T>(
val: unknown,
guard: (v: unknown) => v is T,
msg: string
): asserts val is T {
if (!guard(val)) throw new TypeError(msg);
}
// Guarding arrays
function isArrayOf<T>(arr: unknown, guard: (v: unknown) => v is T): arr is T[] {
return Array.isArray(arr) && arr.every(guard);
}
const isString = (v: unknown): v is string => typeof v === 'string';
const tags: unknown = ['a', 'b', 'c'];
if (isArrayOf(tags, isString)) tags.forEach(t => t.toUpperCase());
// Compose predicates
const and = <T>(g1: (v: unknown) => v is T, g2: (v: T) => boolean) =>
(v: unknown): v is T => g1(v) && g2(v);
const isNonEmptyString = and(isString, s => s.length > 0);Class Patterns & Mixins
Mixin factories compose independent behaviours (timestamping, activation, serialisation) onto any base class without deep inheritance chains. Private class fields (#field) enforce true encapsulation, and static factory methods validate invariants before construction — a pattern that replaces constructors for value-object types.
// Mixin factory — compose behaviours without deep inheritance
type Constructor<T = {}> = new (...args: unknown[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch() { this.updatedAt = new Date(); }
};
}
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
active = false;
activate() { this.active = true; }
deactivate() { this.active = false; }
};
}
class Entity {
constructor(public id: number) {}
}
const TimestampedActivatableEntity = Activatable(Timestamped(Entity));
class User extends TimestampedActivatableEntity {
constructor(id: number, public name: string) { super(id); }
}
const u = new User(1, 'Alice');
u.activate();
u.touch();
// Abstract mixin for serialisation
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
toJSON(): string { return JSON.stringify(this); }
};
}
// Private fields + static factories
class Money {
readonly #amount: number;
readonly #currency: string;
private constructor(amount: number, currency: string) {
this.#amount = amount;
this.#currency = currency;
}
static of(amount: number, currency: string): Money {
if (amount < 0) throw new RangeError('amount must be >= 0');
return new Money(amount, currency);
}
add(other: Money): Money {
if (this.#currency !== other.#currency) throw new Error('currency mismatch');
return Money.of(this.#amount + other.#amount, this.#currency);
}
toString(): string { return `${this.#amount} ${this.#currency}`; }
}Nominal & Opaque Types
TypeScript's structural typing lets two types with identical shapes unify silently. Branded types attach a phantom unique symbol tag to a primitive so the compiler treats UserId and OrderId as distinct — preventing accidental swaps at zero runtime cost. Validation constructors enforce domain invariants at the only place raw values enter the system.
// Structural typing pitfall — two types with same shape are interchangeable
type Meters = number;
type Seconds = number;
function speed(d: Meters, t: Seconds): number { return d / t; }
speed(100, 9.58); // OK — but swapping args silently compiles too
// Branded / opaque types — attach a phantom brand to prevent mixing
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };
type MetersB = Brand<number, 'Meters'>;
type SecondsB = Brand<number, 'Seconds'>;
function toMeters(n: number): MetersB { return n as MetersB; }
function toSeconds(n: number): SecondsB { return n as SecondsB; }
function speedB(d: MetersB, t: SecondsB): number { return d / t; }
const dm = toMeters(100);
const tm = toSeconds(9.58);
speedB(dm, tm); // OK
// speedB(tm, dm); // Error — types are incompatible
// UserId / OrderId — prevent accidental ID swaps
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function getUser(id: UserId): Promise<{ name: string }> {
return fetch(`/users/${id}`).then(r => r.json());
}
const uid = 'u-123' as UserId;
const oid = 'o-456' as OrderId;
getUser(uid); // OK
// getUser(oid); // Error
// Validation constructor — ensures invariants at the boundary
class EmailAddress {
private constructor(public readonly value: string) {}
static parse(raw: string): EmailAddress {
if (!/^[^@]+@[^@]+\.[^@]+$/.test(raw)) throw new Error('invalid email');
return new EmailAddress(raw);
}
}
// Safe integer — proof-carrying type
type SafeInt = Brand<number, 'SafeInt'>;
function safeInt(n: number): SafeInt {
if (!Number.isSafeInteger(n)) throw new RangeError('not a safe integer');
return n as SafeInt;
}Type-Level Testing
The Expect / Equal / NotEqual helpers turn compile-time type assertions into ordinary variables: if a type assertion is wrong, the file fails to compile. This technique, popularised by the tsd and type-challenges projects, lets you write a full test suite for your utility types alongside your source code.
// Type-level test helpers — assert type relationships at compile time
type Expect<T extends true> = T;
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type NotEqual<A, B> = Equal<A, B> extends true ? false : true;
type Extends<A, B> = A extends B ? true : false;
type IsNever<T> = [T] extends [never] ? true : false;
// Usage — if any of these produce a type error, the type is wrong
type _tests = [
Expect<Equal<ReturnType<() => string>, string>>,
Expect<Equal<Awaited<Promise<number>>, number>>,
Expect<NotEqual<string, number>>,
Expect<Extends<'hello', string>>,
Expect<IsNever<never>>,
];
// Testing a custom DeepPartial utility type
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
type _dp = [
Expect<Equal<DeepPartial<{ a: number }>, { a?: number }>>,
Expect<Equal<DeepPartial<{ a: { b: string } }>, { a?: { b?: string } }>>,
];
// Asserting on conditional type distribution
type ToArray<T> = T extends unknown ? T[] : never;
type _ta = [
Expect<Equal<ToArray<string | number>, string[] | number[]>>,
];
// Checking mapped type output — template literal keys
type EventMap = { click: MouseEvent; focus: FocusEvent };
type Handlers = { [K in keyof EventMap as `on${Capitalize<string & K>}`]: (e: EventMap[K]) => void };
type HasOnClick = 'onClick' extends keyof Handlers ? true : false;
// @ts-expect-error — the following would fail if HasOnClick is false
const _h: Expect<Equal<HasOnClick, true>> = true;
// Checking assignability
type Assign<A, B> = A extends B ? B extends A ? true : false : false;
const _str: Expect<Equal<Assign<string, string>, true>> = true;
const _dif: Expect<Equal<Assign<string, number>, false>> = true;
// Verify optional fields survive Partial / Required roundtrip
type WithOptional = { a: string; b?: number };
type Keys = keyof Required<WithOptional>;
const _keys: Expect<Equal<Keys, 'a' | 'b'>> = true;Performance & Type Complexity
Complex generic types can dramatically slow the TypeScript compiler. Key rules: prefer lazy interfaces over eager type aliases for recursive shapes; avoid wide string unions where Record<string, T> suffices; extract repeated conditional checks into named aliases so the compiler can cache instantiations; and use tsc --diagnostics to measure instantiation counts and identify bottlenecks.
// Eager vs. lazy resolution
// Interfaces resolve lazily — better for recursive types and large unions
interface LazyNode { value: number; children: LazyNode[] } // OK
// Type aliases resolve eagerly — guard self-references with a conditional
// type Cycle = { children: Cycle[] } // may slow down the checker
// Avoid wide union explosion — each member multiplies checking work
// Prefer Record/index signatures for open-ended key sets
type ColorMap = Record<string, string>; // fast O(1) lookup
// Prefer single conditional over nested chain
// Slow: T extends A ? X : T extends B ? X : T extends C ? X : never
// Fast: T extends A | B | C ? X : never
type IsJsonPrimitive<T> = T extends string | number | boolean | null ? true : false;
// Extract reused conditionals to named aliases — compiler caches them
type IsObject<T> = T extends object ? true : false;
type Deep<T> = IsObject<T> extends true ? { [K in keyof T]: Deep<T[K]> } : T;
// noUncheckedIndexedAccess — catches missing bounds checks
// (set in tsconfig: 'noUncheckedIndexedAccess': true)
const arr: number[] = [1, 2, 3];
const first: number | undefined = arr[0]; // undefined with the flag
// Prefer unknown over any — keeps the type checker engaged
function safeParse(json: string): unknown {
return JSON.parse(json);
}
// satisfies for literal inference without type widening
const routes = {
home: '/',
about: '/about',
contact: '/contact',
} satisfies Record<string, string>;
routes.home; // type is '/', not string
// Use type guards instead of as casts for safe narrowing
function asString(val: unknown): string {
if (typeof val !== 'string') throw new TypeError('expected string');
return val; // narrowed — no assertion needed
}
// Measure instantiation depth with tsc --diagnostics
// High 'Instantiation count' → simplify recursive or distributive types
// High 'Check time' → reduce union member count or inline helpersTypeScript Compiler API
The typescript package exposes a full compiler API for building codemods, linters, documentation generators, and custom transforms. You can parse source text into an AST, walk nodes with forEachChild, query types through the TypeChecker, and emit transformed output via a custom TransformerFactory — all programmatically, without shelling out to tsc.
// TypeScript Compiler API — programmatic access to the TS compiler
import ts from 'typescript';
// 1. Parse a source file into an AST
const source = ts.createSourceFile(
'example.ts',
'const x: number = 42;',
ts.ScriptTarget.Latest,
true
);
// 2. Walk the AST
function visit(node: ts.Node, depth = 0): void {
const indent = ' '.repeat(depth);
console.log(`${indent}${ts.SyntaxKind[node.kind]}`);
ts.forEachChild(node, child => visit(child, depth + 1));
}
visit(source);
// 3. Create a Program and type-check
const host = ts.createCompilerHost({ strict: true });
const program = ts.createProgram(['./src/index.ts'], { strict: true }, host);
const checker = program.getTypeChecker();
const diagnostics = ts.getPreEmitDiagnostics(program);
diagnostics.forEach(d => {
const msg = ts.flattenDiagnosticMessageText(d.messageText, '\\n');
if (d.file && d.start !== undefined) {
const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
console.error(`${d.file.fileName}(${line + 1},${character + 1}): ${msg}`);
} else {
console.error(msg);
}
});
// 4. Extract type info with the checker
function getNodeType(node: ts.Node): string {
const type = checker.getTypeAtLocation(node);
return checker.typeToString(type);
}
// 5. Custom transformer — add readonly to all class properties
function addReadonlyTransformer(context: ts.TransformationContext) {
return (rootNode: ts.SourceFile) => {
function visitNode(node: ts.Node): ts.Node {
if (ts.isPropertyDeclaration(node)) {
const mods = [ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)];
return ts.factory.updatePropertyDeclaration(
node, mods, node.name, node.questionToken, node.type, node.initializer
);
}
return ts.visitEachChild(node, visitNode, context);
}
return ts.visitNode(rootNode, visitNode) as ts.SourceFile;
};
}
// 6. Emit transformed output
const transformResult = ts.transform(source, [addReadonlyTransformer]);
const printer = ts.createPrinter();
const output = printer.printFile(transformResult.transformed[0]);
console.log(output);`Best Practices
Type Design
// Prefer interfaces for object shapes (lazy resolution, better error messages)
interface User {
readonly id: string; // readonly — immutable after creation
name: string;
email: string;
}
// Use type for unions, intersections, and aliases
type ID = string | number;
type AdminUser = User & { permissions: string[] };
// Avoid any — use unknown and narrow instead
function processInput(val: unknown): string {
if (typeof val === 'string') return val.toUpperCase();
if (typeof val === 'number') return val.toFixed(2);
throw new TypeError('unsupported input type');
}
// Use never to enforce exhaustiveness
type Shape = { kind: 'circle'; r: number } | { kind: 'rect'; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.r ** 2;
case 'rect': return s.w * s.h;
default: throw new Error('unhandled shape: ' + (s as never));
}
}
// Const assertions — preserve literal types
const CONFIG = { env: 'production', port: 3000 } as const;
// Prefer discriminated unions over optional fields
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// Branded types for domain IDs
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;Generics & Utilities
// Constrain with extends before using
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Provide default type params for common cases
type ApiResponse<T = unknown> = { data: T; status: number; error?: string };
// Use built-in utility types — avoid reinventing them
type UserDraft = Partial<User>;
type ImmUser = Readonly<User>;
type UserPreview = Pick<User, 'id' | 'name'>;
type UserInput = Omit<User, 'id'>;
type RoleMap = Record<string, string[]>;
// Extract a generic only when it is used in 3+ places
type Paginated<T> = { items: T[]; total: number; page: number; pageSize: number };
// Keep generic names meaningful
interface Repository<TEntity> {
findById(id: string): Promise<TEntity | null>;
save(entity: TEntity): Promise<TEntity>;
delete(id: string): Promise<void>;
}
// Conditional types for type-level logic
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Awaited2<T> = T extends Promise<infer V> ? Awaited2<V> : T;
// Document expected constraints in JSDoc
/**
* Sorts items by a numeric or string key.
* @typeParam TItem - must have a comparable key K
*/
function sortBy<TItem, K extends keyof TItem>(
items: TItem[],
key: K
): TItem[] {
return [...items].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0));
}Narrowing & Guards
// Prefer discriminated unions over instanceof checks for domain types
type ApiEvent =
| { type: 'request'; url: string; method: string }
| { type: 'response'; status: number; body: string }
| { type: 'error'; message: string; code: number };
function handleEvent(e: ApiEvent): void {
switch (e.type) {
case 'request': console.log(e.method, e.url); break;
case 'response': console.log(e.status); break;
case 'error': console.error(e.message); break;
}
}
// Reusable type predicates
function isNonNull<T>(val: T | null | undefined): val is T {
return val !== null && val !== undefined;
}
const ids: (string | null)[] = ['a', null, 'b'];
const validIds = ids.filter(isNonNull); // string[]
// assertNever for exhaustiveness
function assertNever(x: never): never {
throw new Error('unhandled case: ' + JSON.stringify(x));
}
// Narrow unknown at system boundaries
function parseConfig(raw: unknown): { host: string; port: number } {
if (
typeof raw !== 'object' || raw === null ||
!('host' in raw) || typeof (raw as { host: unknown }).host !== 'string' ||
!('port' in raw) || typeof (raw as { port: unknown }).port !== 'number'
) throw new TypeError('invalid config');
return raw as { host: string; port: number };
}
// satisfies — validate shape without widening literal types
const palette = {
primary: '#3b82f6',
secondary: '#6366f1',
} satisfies Record<string, string>;
palette.primary; // type is '#3b82f6', not string
// Use 'in' operator for duck-typing object shapes
function isCat(animal: unknown): animal is { meow(): void } {
return typeof animal === 'object' && animal !== null && 'meow' in animal;
}Async & Error Handling
// Always annotate async return types explicitly
async function fetchUser(id: string): Promise<User> {
const res = await fetch('/api/users/' + id);
if (!res.ok) throw new Error('fetch failed: ' + res.status);
return res.json() as Promise<User>;
}
// Result<T, E> for expected errors — no-throw control flow
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
async function safeParseUser(id: string): Promise<Result<User>> {
try {
const user = await fetchUser(id);
return { ok: true, value: user };
} catch (e: unknown) {
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
}
}
// catch as unknown in strict mode — never assume error shape
async function run(): Promise<void> {
try {
await fetchUser('123');
} catch (e: unknown) {
if (e instanceof Error) console.error(e.message);
else console.error('unknown error', e);
}
}
// Awaited<T> for unwrapping nested promises
type UserResult = Awaited<ReturnType<typeof fetchUser>>; // User
// AbortSignal in API boundaries
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
// Async generator for streaming data
async function* streamLines(url: string): AsyncGenerator<string> {
const res = await fetch(url);
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop()!;
for (const line of lines) yield line;
}
}tsconfig & Tooling
// tsconfig.json — recommended strict baseline for new projects
// {
// 'compilerOptions': {
// 'strict': true, // enables all strict checks
// 'noUncheckedIndexedAccess': true, // arr[i] is T | undefined
// 'exactOptionalPropertyTypes': true, // no implicit undefined assignment
// 'noImplicitOverride': true, // require 'override' keyword
// 'moduleResolution': 'bundler', // modern Vite/esbuild/webpack 5
// 'module': 'ESNext',
// 'target': 'ES2022',
// 'lib': ['ES2022', 'DOM'],
// 'paths': { '@/*': ['./src/*'] }, // import aliases
// 'skipLibCheck': true,
// 'declaration': true,
// 'sourceMap': true
// }
// }
// Run type-check without emitting in CI
// tsc --noEmit
// Profile slow types
// tsc --diagnostics 2>&1 | grep -E 'Instantiation|Check time'
// ESLint typescript plugin — catches common mistakes at lint time
// npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
// .eslintrc: { 'extends': ['plugin:@typescript-eslint/recommended-type-checked'] }
// Project references for monorepos — incremental builds
// tsconfig.json at root:
// {
// 'references': [
// { 'path': './packages/core' },
// { 'path': './packages/api' },
// { 'path': './packages/ui' }
// ]
// }
// Build all: tsc --build
// Watch: tsc --build --watch
// noUncheckedIndexedAccess in practice
const items: string[] = ['a', 'b'];
const first: string | undefined = items[0]; // explicit undefined with flag
if (first !== undefined) first.toUpperCase(); // safe after narrowPatterns & Architecture
// Prefer explicit named imports over barrel re-exports for deep trees
// Barrel files are fine for small, stable public APIs
// Avoid: import { everything } from '@/components' — slows down bundlers
// Prefer: import { Button } from '@/components/Button'
// Declaration merging — augment third-party types without forking
import 'express';
declare module 'express' {
interface Request {
currentUser?: { id: string; role: string };
}
}
// Const enums — avoid with isolatedModules (esbuild / Babel can't inline them)
// Use plain string union or object enum instead:
const Direction = { Up: 'UP', Down: 'DOWN', Left: 'LEFT', Right: 'RIGHT' } as const;
type Direction = typeof Direction[keyof typeof Direction];
// Prefer abstract classes over interfaces when sharing implementation
abstract class BaseRepository<T> {
abstract findById(id: string): Promise<T | null>;
async findOrThrow(id: string): Promise<T> {
const item = await this.findById(id);
if (!item) throw new Error('not found: ' + id);
return item;
}
}
// Runtime validation at boundaries — generate types from schemas
// import { z } from 'zod';
// const UserSchema = z.object({ id: z.string(), name: z.string(), age: z.number() });
// type User = z.infer<typeof UserSchema>; // type comes from the schema
// const user = UserSchema.parse(req.body); // validated at runtime
// Never write types first and try to match them with validators later
// Schema -> type (single source of truth)
// Type -> schema (duplication — risk of drift)
// Use satisfies for config objects to catch typos without widening
type Route = { path: string; component: string; exact?: boolean };
const routes = [
{ path: '/', component: 'Home', exact: true },
{ path: '/about', component: 'About' },
{ path: '/users', component: 'Users' },
] satisfies Route[];