Skip to content
Back to skills

Typescript

ASecurity

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...

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 1, 2026
developmentjavascripttypescriptgojavabashvuenextjsnodeexpressdebugging

Works with

  • api

Security analysis

A100/100

Scanned September 1, 2026

npx -y skills add nrdxp/predicate --skill typescript --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Typescript?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Typescript
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nrdxp-typescript/badge)](https://www.skillsdirectory.com/skills/nrdxp-typescript)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…