| name | typescript |
| description | Idiomatic programming style, patterns, and conventions for TypeScript and JavaScript development.
Trigger when:
- Writing, refactoring, reviewing, or debugging TypeScript or JavaScript code.
- Files matching the patterns **/*.ts, **/*.tsx, **/*.js, **/*.jsx, package.json are in the workspace or referenced.
- Tasks involve: tsc, npm, yarn, pnpm, bun, eslint, prettier, vite, next.js.
- Prompt contains keywords: typescript, ts, js, javascript, interface, type, async, await, promise, es6, node, deno.
|
TypeScript / JavaScript Language Idioms
Work with the language. Embrace its asynchronous nature, its structural type system, and its functional roots. Fight the urge to write Java-in-TypeScript.
Core Philosophy
- Immutability by default —
const everything, spread to copy, never mutate in place
- Type safety without ceremony — let inference work, annotate at boundaries
- Composition over inheritance — functions, modules, and interfaces beat class hierarchies
- Explicit over implicit — no coercion tricks, no hidden state, no magic
References and Variables
The Rules
| Keyword | Usage | Rationale |
|---|
const | Default for all bindings | Prevents reassignment, signals intent |
let | Only when reassignment is unavoidable | Loops, accumulators, state machines |
var | Prohibited | Function-scoped, hoists, leaks out of blocks |
Destructuring and Spread
Use destructuring to extract what you need. Use spread to copy without mutation:
const { name, age } = getUser();
const updated = { ...config, timeout: 5000 };
const extended = [...items, newItem];
config.timeout = 5000;
items.push(newItem);
Use Object.hasOwn(obj, key) instead of obj.hasOwnProperty(key) — the latter can be shadowed.
Functions
Arrow Functions
Use arrow functions for callbacks and short expressions. They preserve lexical this:
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
const names = users.map((u) => u.name);
const names = users.map(function (u) {
return u.name;
});
Use function declarations for top-level named functions (they hoist and have clear stack traces).
Options Objects
When a function's parameter list becomes unwieldy, use a single options object:
function createServer(
port: number,
host: string,
ssl: boolean,
timeout: number,
);
interface ServerOptions {
port: number;
host?: string;
ssl?: boolean;
timeout?: number;
}
function createServer(options: ServerOptions);
Rest Parameters
Use rest syntax. The arguments object is prohibited:
function log(level: string, ...messages: string[]) {}
function log() {
console.log(arguments);
}
Pure Functions
Prefer functions that return values based solely on inputs. Reserve side effects for explicit boundaries (I/O, event handlers, logging).
Functional Iterators
Use map, filter, reduce to build declarative pipelines that produce new data:
const active = users.filter((u) => u.isActive).map((u) => u.email);
const active: string[] = [];
users.forEach((u) => {
if (u.isActive) active.push(u.email);
});
forEach is acceptable for genuine side effects (logging, DOM, events) — not for data transformation.
Async Patterns
JavaScript is async-first. Treat asynchrony as the normal case, not the exception.
async/await
Prefer async/await over .then() chains:
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new HttpError(res.status);
return res.json();
}
function fetchUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) throw new HttpError(res.status);
return res.json();
});
}
Concurrent Operations
| Pattern | Behavior | Use When |
|---|
Promise.all | Fails fast on first rejection | All must succeed |
Promise.allSettled | Waits for all, reports each | Partial failure is acceptable |
Promise.race | Resolves/rejects with first | Timeouts, fastest-wins |
Cancellation
Use AbortController for cancellable operations:
const controller = new AbortController();
const res = await fetch(url, { signal: controller.signal });
controller.abort();
Modules
ES Modules Only
Use import/export. CommonJS (require) is legacy.
Named Exports
Prefer named exports. They enable compile-time reference checks and explicit dependency graphs:
export function parse(input: string): Token[] {
}
export interface Token {
type: string;
value: string;
}
export default function parse(input: string): Token[] {
}
Default exports are acceptable for framework conventions (e.g., Vue SFCs, Next.js pages) but never preferred.
Barrel Files
Use barrel files (index.ts) sparingly. They defeat tree-shaking and create circular dependency traps. Export directly from source modules when possible.
Type System
Strict Mode
Enable strict: true in tsconfig.json. This is non-negotiable — it activates strictNullChecks, noImplicitAny, and other critical checks.
Type Inference vs. Annotation
Let TypeScript infer when the type is obvious. Annotate at boundaries — function parameters, return types, and public APIs:
const count = 0;
const name = "alice";
function parseConfig(raw: string): Config {
}
unknown over any
any disables type checking. Use unknown and narrow explicitly:
function handle(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase());
}
}
function handle(input: any) {
console.log(input.toUpperCase());
}
Discriminated Unions
Model variant state with a literal discriminant. This enables exhaustive switch checking:
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
function handle(result: Result<string>) {
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error);
}
}
satisfies
Validate that a value conforms to a type without widening:
const routes = {
home: "/",
about: "/about",
users: "/users",
} satisfies Record<string, string>;
as const
Use as const for immutable literal types. Replaces enums in most cases:
const Status = {
Active: "active",
Inactive: "inactive",
Pending: "pending",
} as const;
type Status = (typeof Status)[keyof typeof Status];
Utility Types
Use built-in utility types to derive types from existing ones:
| Type | Purpose | Example |
|---|
Partial<T> | All properties optional | Patch/update payloads |
Required<T> | All properties required | Validated config |
Readonly<T> | All properties readonly | Frozen state |
Pick<T, K> | Subset of properties | API response shaping |
Omit<T, K> | Exclude properties | Remove internal fields |
Record<K, V> | Map of key-value pairs | Lookup tables |
Branded Types
Simulate nominal typing for domain safety:
type UserId = string & { readonly __brand: unique symbol };
type PostId = string & { readonly __brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
Type Erasure
TypeScript types are stripped at build time. They cannot guard runtime execution.
- Prefer erasable-only syntax — types, interfaces, type aliases all vanish cleanly
- Avoid enums — they emit runtime JavaScript; use
as const objects or union types instead
- Use
#field for private — not the private keyword, which is TypeScript-only
- Validate at boundaries — external data (API responses, user input) must be validated at runtime, not just typed
class User {
#email: string;
constructor(email: string) {
this.#email = email;
}
}
class User {
private email: string;
constructor(email: string) {
this.email = email;
}
}
Error Handling
Custom Error Classes
Extend Error with domain-specific types. Always set name:
class HttpError extends Error {
constructor(
public readonly status: number,
message?: string,
) {
super(message ?? `HTTP ${status}`);
this.name = "HttpError";
}
}
Async Error Handling
Catch at the appropriate level. Don't swallow errors silently:
try {
const user = await fetchUser(id);
} catch (err) {
if (err instanceof HttpError && err.status === 404) {
return null;
}
throw err;
}
Error Narrowing
TypeScript's catch clause types as unknown. Narrow before accessing:
try {
await riskyOperation();
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
} else {
console.error("Unknown error:", err);
}
}
Naming Conventions
| Scope | Style | Example |
|---|
| Classes, interfaces, type aliases | PascalCase | UserAccount, ServerOptions |
| Functions, variables, properties | camelCase | calculateTotal, isActive |
| Exported constants (true invariants) | SCREAMING_SNAKE | MAX_RETRIES, API_VERSION |
| Private class fields | #camelCase | #connectionPool |
| Generic type parameters | Single uppercase or T-prefix | T, K, TResult |
Equality
Use strict equality (=== / !==). Never rely on abstract coercion (==).
Use shortcuts for booleans (if (isValid)) but explicit comparisons for strings and numbers (if (name !== ""), if (count > 0)) to prevent coercion surprises.
Nullish Handling
Prefer ?? over || for defaults — || coerces 0, "", and false to falsy:
const timeout = options.timeout ?? 3000;
const timeout = options.timeout || 3000;
Use optional chaining (?.) for safe property access.
Anti-Patterns
| Anti-Pattern | Description | Remedy |
|---|
any leakage | Using any to silence the compiler | Use unknown and narrow |
| Enum abuse | TypeScript enums that emit runtime code | as const objects or union types |
| Class-heavy OOP | Porting Java patterns (abstract classes, deep hierarchies) | Composition, interfaces, plain functions |
| Barrel file sprawl | Re-exporting everything through index.ts | Direct imports from source modules |
| Swallowed errors | Empty catch {} blocks | Handle, log, or rethrow |
| Type assertions | as Type to override the compiler | Annotations (const x: Type) or narrowing |
| Mutation in map/filter | Side effects inside declarative pipelines | forEach for effects, map for transforms |
Tooling
tsc --noEmit
npx eslint .
npx prettier --check .
npx prettier --write .
npx biome check .
Formatting is not a debate. Pick prettier or biome and enforce it in CI.
Quick Reference
const by default — let only when unavoidable, var never
strict: true — always, no exceptions
unknown over any — narrow explicitly
async/await — not .then() chains
- Named exports — not default exports
as const — not enums
#field — not private keyword
=== — not ==
?? — not || for defaults
- Spread to copy — never mutate the original
- Annotate boundaries — infer the rest
These idioms refine but are subordinate to the Code-Edit Constraints.