TypeScript
Best practices for writing modern, type-safe TypeScript. Covers type modeling, modern compiler features (5.5-5.8), error handling, module patterns, and project configuration.
This skill targets .ts and .tsx files. For plain JavaScript with JSDoc type annotations, use the JavaScript skill instead.
Project Setup
tsconfig.json
Enable strict mode in tsconfig.json for every TypeScript project:
{
"compilerOptions": {
"strict": true,
"target": "ES2024",
"module": "nodenext",
"moduleResolution": "nodenext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
Adjust target, module, and moduleResolution to match the project's runtime:
- •Node.js projects:
"module": "nodenext","moduleResolution": "nodenext" - •Bundler-driven projects:
"module": "preserve","moduleResolution": "bundler" - •Direct execution (Node.js 22.6+): Add
"erasableSyntaxOnly": trueto ensure all TS-specific syntax can be stripped without altering runtime behavior. Avoidenum,namespace, and parameter properties (constructor(public x: number)) when this flag is enabled.
Notable Compiler Options (5.5-5.8)
| Option | Version | Purpose |
|---|---|---|
noUncheckedSideEffectImports | 5.6 | Errors on unresolvable import "polyfill" |
erasableSyntaxOnly | 5.8 | Restricts to type-erasable syntax for direct execution |
verbatimModuleSyntax | 5.4 | Enforces explicit import type for type-only imports |
noUncheckedIndexedAccess | 4.1 | Adds undefined to index signature results |
Type Modeling
Prefer Inference Over Annotation
Let TypeScript infer types when the result is unambiguous. Annotate return types on public API surfaces and when inference would be too wide:
// Inferred — no annotation needed
const count = items.length;
const doubled = numbers.map(n => n * 2);
// Annotate: public API, inference would be too wide
function parseConfig(raw: string): AppConfig {
return JSON.parse(raw) as AppConfig;
}
Discriminated Unions
Model variant types using a shared literal discriminant property. Use exhaustiveness checking with never to catch unhandled cases:
type Result<T> =
| { kind: 'success'; value: T }
| { kind: 'error'; error: Error };
function handle<T>(result: Result<T>): T {
switch (result.kind) {
case 'success': return result.value;
case 'error': throw result.error;
default: {
const _exhaustive: never = result;
throw new Error(`Unhandled case: ${_exhaustive}`);
}
}
}
The satisfies Operator
Use satisfies to validate a value conforms to a type while preserving narrower inference. Prefer satisfies over as for type validation:
type Route = { path: string; auth: boolean };
type Routes = Record<string, Route>;
// Type annotation: loses key knowledge
const annotated: Routes = { home: { path: '/', auth: false } };
annotated.home; // Route — no key autocomplete
// satisfies: validates AND preserves literal keys
const checked = {
home: { path: '/', auth: false },
dashboard: { path: '/dash', auth: true },
} satisfies Routes;
checked.home; // { path: string; auth: boolean } — keys autocomplete
// as const satisfies: preserves literal values too
const frozen = {
home: { path: '/', auth: false },
} as const satisfies Routes;
frozen.home.path; // '/' (literal type)
Branded Types
Create nominally distinct types from the same underlying type to prevent accidental interchange:
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'>;
function createUserId(id: string): UserId {
if (!id) throw new Error('Invalid user ID');
return id as UserId;
}
function lookupUser(id: UserId): User { /* ... */ }
lookupUser(createUserId('abc')); // OK
lookupUser('abc'); // Error: string is not UserId
NoInfer<T>
Use NoInfer (TS 5.4+) to control which parameter TypeScript infers a generic from:
function createFSM<TState extends string>(
states: TState[],
initial: NoInfer<TState> // Must be one of the states, inferred from `states`
) { /* ... */ }
createFSM(['idle', 'running', 'done'], 'idle'); // OK
createFSM(['idle', 'running', 'done'], 'invalid'); // Error
Const Type Parameters
Use const type parameters (TS 5.0+) to infer literal types instead of widened types:
function createRoutes<const T extends readonly Route[]>(routes: T) {
return routes;
}
// Infers tuple of literal types, not Route[]
For advanced patterns including template literal types, mapped types, conditional types, and complex generics, consult references/type-patterns.md.
Modern Language Features
Inferred Type Predicates (TS 5.5)
TypeScript 5.5 infers type predicates from function bodies. Manual type guards are no longer needed for simple narrowing:
// TS 5.5+: automatic inference, no annotation needed const validUsers = users.filter(user => user !== null); // Inferred as User[] (not (User | null)[])
Inference requires: a single return statement, no parameter mutation, and a boolean expression tied to a parameter refinement. Truthiness checks (!!x) infer predicates for object types but not for numbers (where 0 is falsy).
Explicit Resource Management (Stage 3)
Use using and await using for automatic resource cleanup:
function readFile(path: string) {
using handle = openFile(path); // Symbol.dispose called at scope exit
return handle.read();
}
async function connectDB() {
await using conn = await pool.getConnection(); // Symbol.asyncDispose called
return conn.query('SELECT 1');
}
Requires "lib": ["esnext.disposable"] in tsconfig. Check target runtime support — Chrome 134+, not yet cross-browser.
Error Handling
Error Cause Chains
Wrap errors with context using the cause option:
try {
const data = JSON.parse(raw);
} catch (err) {
throw new Error('Failed to parse config', { cause: err });
}
Result Pattern
Prefer discriminated union results over thrown exceptions for expected failure cases:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
return { ok: true, value: await res.json() };
}
Reserve throw for truly exceptional / unrecoverable situations. Use Result for operations with expected failure modes (network requests, parsing, validation).
Module Patterns
ESM Conventions
- •Use
"type": "module"inpackage.json - •Prefer named exports over default exports for discoverability and refactoring
- •Use the
"exports"field to define the package's public API - •Avoid barrel files (
index.tsre-exporting everything) in application code — they degrade tree-shaking, slow builds, and risk circular dependencies - •Prefer direct imports:
import { thing } from './utils/thing.js' - •Use
import type(orverbatimModuleSyntax) for type-only imports
Class Patterns
Use private fields (#field) and accessor keywords:
class Elevator {
#currentFloor: number;
#destination: number | null = null;
constructor(startFloor: number) {
this.#currentFloor = startFloor;
}
get currentFloor(): number {
return this.#currentFloor;
}
}
TypeScript 7 / Compiler Rewrite
The TypeScript compiler is being rewritten in Go (Project Corsa). TypeScript 7 is expected in 2026 with ~10x faster builds. Key migration considerations:
- •
strictenabled by default - •Minimum target
es2015(no morees5) - •
node10module resolution removed — usenodenextorbundler - •Standard LSP protocol replaces custom TSServer protocol
- •
enum,namespace, and parameter properties remain supported but cannot be type-erased (relevant forerasableSyntaxOnly)
Avoid patterns that may complicate migration: deeply nested namespaces, complex enum merging, and reliance on custom TSServer APIs.
Additional Resources
- •
references/type-patterns.md— Advanced type patterns: template literal types, mapped types, conditional types, complex generics, utility types, and type narrowing techniques. - •
references/api-reference.md— Quick reference for ES2024/2025 features, Web APIs, and TypeScript compiler options with usage examples and runtime support status.