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, d...
Scanned 9/1/2026
Install to Claude Code
npx -y skills add nrdxp/predicate --skill typescript --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Typescript?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/nrdxp-typescript)More formats (shields.io, HTML) on the badges page.
---
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:
```typescript
// ✅ Destructure at the call site
const { name, age } = getUser();
// ✅ Shallow-copy with spread — never mutate the original
const updated = { ...config, timeout: 5000 };
const extended = [...items, newItem];
// ❌ Mutating the original
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`:
```typescript
// ✅ Arrow for callbacks
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
// ✅ Arrow for inline transforms
const names = users.map((u) => u.name);
// ❌ Legacy — don't use `function` for callbacks
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:
```typescript
// ❌ Positional parameter overload
function createServer(
port: number,
host: string,
ssl: boolean,
timeout: number,
);
// ✅ Options object — extensible, self-documenting
interface ServerOptions {
port: number;
host?: string;
ssl?: boolean;
timeout?: number;
}
function createServer(options: ServerOptions);
```
### Rest Parameters
Use rest syntax. The `arguments` object is prohibited:
```typescript
// ✅ Rest gives you a real Array
function log(level: string, ...messages: string[]) {}
// ❌ arguments is not a real Array and has no type safety
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:
```typescript
// ✅ Declarative pipeline — new array, no mutation
const active = users.filter((u) => u.isActive).map((u) => u.email);
// ❌ Imperative mutation
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:
```typescript
// ✅ Linear, readable
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();
}
// ❌ Nested chains obscure flow
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:
```typescript
const controller = new AbortController();
const res = await fetch(url, { signal: controller.signal });
// Cancel from elsewhere
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:
```typescript
// ✅ Named — IDE catches broken imports immediately
export function parse(input: string): Token[] {
/* ... */
}
export interface Token {
type: string;
value: string;
}
// ⚠️ Default — permits arbitrary rename at import site
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:
```typescript
// ✅ Inferred — obvious from the literal
const count = 0;
const name = "alice";
// ✅ Annotated — boundary, not obvious
function parseConfig(raw: string): Config {
/* ... */
}
```
### `unknown` over `any`
`any` disables type checking. Use `unknown` and narrow explicitly:
```typescript
// ✅ Forces narrowing before use
function handle(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase());
}
}
// ❌ Silently accepts anything — bugs hide here
function handle(input: any) {
console.log(input.toUpperCase()); // runtime bomb
}
```
### Discriminated Unions
Model variant state with a literal discriminant. This enables exhaustive `switch` checking:
```typescript
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
function handle(result: Result<string>) {
if (result.ok) {
console.log(result.value); // narrowed to { ok: true; value: string }
} else {
console.error(result.error); // narrowed to { ok: false; error: Error }
}
}
```
### `satisfies`
Validate that a value conforms to a type without widening:
```typescript
// ✅ Validates shape, preserves literal types
const routes = {
home: "/",
about: "/about",
users: "/users",
} satisfies Record<string, string>;
// TypeScript still knows routes.home is "/" (literal), not just string
```
### `as const`
Use `as const` for immutable literal types. Replaces enums in most cases:
```typescript
// ✅ as const object — runtime value + narrow types
const Status = {
Active: "active",
Inactive: "inactive",
Pending: "pending",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// "active" | "inactive" | "pending"
```
### 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:
```typescript
type UserId = string & { readonly __brand: unique symbol };
type PostId = string & { readonly __brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId; // validated assertion at construction
}
// Now UserId and PostId are incompatible at compile time
```
### 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
```typescript
// ✅ Native private — survives type erasure
class User {
#email: string;
constructor(email: string) {
this.#email = email;
}
}
// ❌ TypeScript private — erased, accessible at runtime
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`:
```typescript
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:
```typescript
// ✅ Catch and handle or rethrow with context
try {
const user = await fetchUser(id);
} catch (err) {
if (err instanceof HttpError && err.status === 404) {
return null; // expected case
}
throw err; // unexpected — propagate
}
```
### Error Narrowing
TypeScript's `catch` clause types as `unknown`. Narrow before accessing:
```typescript
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:
```typescript
// ✅ Only falls through on null/undefined
const timeout = options.timeout ?? 3000;
// ❌ Falls through on 0, "", false
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
```bash
tsc --noEmit # Type-check without emitting
npx eslint . # Lint
npx prettier --check . # Format check
npx prettier --write . # Auto-format
npx biome check . # All-in-one (alternative to eslint + prettier)
```
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](../../ambient.md#code-edit-constraints).Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!