Skip to content
cccskills
FreePublished on GitHub

typescript

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.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md14.4 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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

KeywordUsageRationale
constDefault for all bindingsPrevents reassignment, signals intent
letOnly when reassignment is unavoidableLoops, accumulators, state machines
varProhibitedFunction-scoped, hoists, leaks out of blocks

Destructuring and Spread

Use destructuring to extract what you need. Use spread to copy without mutation:

// ✅ 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:

// ✅ 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:

// ❌ 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:

// ✅ 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:

// ✅ 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:

// ✅ 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

PatternBehaviorUse When
Promise.allFails fast on first rejectionAll must succeed
Promise.allSettledWaits for all, reports eachPartial failure is acceptable
Promise.raceResolves/rejects with firstTimeouts, fastest-wins

Cancellation

Use AbortController for cancellable operations:

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:

// ✅ 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:

// ✅ 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:

// ✅ 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:

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:

// ✅ 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:

// ✅ 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:

TypePurposeExample
Partial<T>All properties optionalPatch/update payloads
Required<T>All properties requiredValidated config
Readonly<T>All properties readonlyFrozen state
Pick<T, K>Subset of propertiesAPI response shaping
Omit<T, K>Exclude propertiesRemove internal fields
Record<K, V>Map of key-value pairsLookup 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; // 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
// ✅ 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:

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:

// ✅ 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:

try {
  await riskyOperation();
} catch (err) {
  if (err instanceof Error) {
    console.error(err.message);
  } else {
    console.error("Unknown error:", err);
  }
}

Naming Conventions

ScopeStyleExample
Classes, interfaces, type aliasesPascalCaseUserAccount, ServerOptions
Functions, variables, propertiescamelCasecalculateTotal, isActive
Exported constants (true invariants)SCREAMING_SNAKEMAX_RETRIES, API_VERSION
Private class fields#camelCase#connectionPool
Generic type parametersSingle uppercase or T-prefixT, 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:

// ✅ 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-PatternDescriptionRemedy
any leakageUsing any to silence the compilerUse unknown and narrow
Enum abuseTypeScript enums that emit runtime codeas const objects or union types
Class-heavy OOPPorting Java patterns (abstract classes, deep hierarchies)Composition, interfaces, plain functions
Barrel file sprawlRe-exporting everything through index.tsDirect imports from source modules
Swallowed errorsEmpty catch {} blocksHandle, log, or rethrow
Type assertionsas Type to override the compilerAnnotations (const x: Type) or narrowing
Mutation in map/filterSide effects inside declarative pipelinesforEach for effects, map for transforms

Tooling

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.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

SOP for auditing AI-generated code. Trigger when: - Reviewing, refactoring, or cleaning up AI-generated code to prevent regressions or hallucinated APIs. - Prompt contains: /ai-audit, code audit, AI cleanup, common flaws.

The English summary is being prepared. Showing the original description.

nrdxp/predicate10Updated Sep 1, 2026

Protocol for auditing API surface coherence and type safety. Trigger when: - Evaluating API designs, interface type safety, or design elegance. - Prompt contains: /api-audit, API surface, API coherence, type safety.

The English summary is being prepared. Showing the original description.

nrdxp/predicate10Updated Sep 1, 2026

Normative sufficiency conditions for Initial Boundary Conditions (IBCs) and the SOP for the cheap-tier boundary refinement loop (/boundary). Trigger when: - Crafting, auditing, or refining a prompt/IBC destined for an expensive (architect-class) model or an autonomous worker dispatch. - Evaluating whether a task frame is sufficient to bound an agent walk. - Prompt contains: /boundary, IBC, initial boundary condition, boundary contract, sufficiency conditions, worker prompt, prompt refinement.

The English summary is being prepared. Showing the original description.

nrdxp/predicate10Updated Sep 1, 2026

SOP for the architect-tier campaign workflow (/campaign): exhaustive survey, mitigation planning, tiered orchestration, and reconciliation. Trigger when: - Running a multi-workstream initiative where an expensive architect-tier council surveys, plans, emits worker prompts, and judges landed work. - Conducting production-readiness assessments that fan out into autonomous mitigation dispatches across model tiers. - Prompt contains: /campaign, campaign workflow, survey, orchestrate, reconcile, premise freshness, tier routing, worker IBC, scratch.

The English summary is being prepared. Showing the original description.

nrdxp/predicate10Updated Sep 1, 2026

Maintain and update the persistent project chronicle (docs/chronicle.md). Trigger when: - The human requests a history summary or chronicle update. - Starting work on a new codebase and needing context on its evolution. - Prompt contains keywords: /chronicle, chronicle, project history, git log summary, history summary.

The English summary is being prepared. Showing the original description.

nrdxp/predicate10Updated Sep 1, 2026

Rules, conventions, and constraints for formatting git commit messages and committing at logical boundaries. Trigger when: - Drafting, revising, or validating git commit messages. - Pausing at commit boundaries under the CORE or CONTINUE workflows. - Evaluating whether a changeset should be split into multiple commits. - Prompt contains keywords: commit message, git commit, conventional commits, commit hygiene, commit guidelines, logical boundary, spaghetti diff, atomic commit, commit boundary.

The English summary is being prepared. Showing the original description.

nrdxp/predicate10Updated Sep 1, 2026

See all skills by nrdxp

Report a problem with this skill