Skip to content
Back to skills

Typescript Conventions

ASecurity

Use when a ticket adds or changes TypeScript/JavaScript code and it must follow the repo's TS conventions — strict mode typing, typescript-eslint type-aware rules, async correctness (no floating promises, bounded fan-out), boundary validation instead of casts, module/import hygiene, Node resource and concurrency safety, and idiomatic patterns. Invoke for "add this in TypeScript", "fix the type errors", "tighten the types on X", or as the language pack for any TS/JS change or TS/JS review.

  • 2 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added September 5, 2026
ai-agentsjavascripttypescriptgojavareactvueangularnextjsnodedatabase

Works with

  • mcp

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 29, 2026

npx -y skills add tmj-90/gaffer --skill typescript-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Typescript Conventions?

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

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

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-conventions
description: Use when a ticket adds or changes TypeScript/JavaScript code and it must follow the repo's TS conventions — strict mode typing, typescript-eslint type-aware rules, async correctness (no floating promises, bounded fan-out), boundary validation instead of casts, module/import hygiene, Node resource and concurrency safety, and idiomatic patterns. Invoke for "add this in TypeScript", "fix the type errors", "tighten the types on X", or as the language pack for any TS/JS change or TS/JS review.
stack: [typescript, javascript, node, react, next, nextjs, vue, svelte, angular, react-native, expo]
area: language
---

# Write idiomatic, strict TypeScript

TypeScript protects you only as far as its types are honest: strict mode on, external
data validated rather than cast, and every promise awaited or handled. For the builder and the reviewer of a TS/JS diff; the repo's config and existing code win over it. UI framework patterns live in their own packs (the `react-patterns`
skill, the frontend packs).

## Procedure

1. **Discover the repo's conventions first.** Call `search_lore` for TS conventions. Read
   `tsconfig*.json` (`strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`,
   `module`/`moduleResolution`, `verbatimModuleSyntax`, `erasableSyntaxOnly`), the
   TypeScript version (6.0 turned `strict` on by default and deprecated `baseUrl`,
   `moduleResolution: node`; 7.0 removed them and ships the native `tsc`),
   `package.json` (`type`, `scripts`, `engines`, `packageManager`), the lockfile that names
   the package manager (`package-lock.json` npm, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`),
   `eslint.config.*` or `biome.json`, Prettier config, the test runner config, and
   workspace tooling (`turbo.json`, `nx.json`). Copy a sibling module and its test.
2. **Pin the exact commands** from `package.json` scripts and CI (the `run-tests` and
   `run-lint` skills). Use the defaults below only when the repo defines none.
3. **Write the change with the idioms below**, then walk the concurrency section for every
   promise, shared object, stream, timer, file and outbound call you touched.
4. **Test each acceptance criterion's own behaviour.** One test per AC that fails without
   your change, plus its error path. If the AC involves shared state or persistence, add a
   test that fires N concurrent requests (`Promise.all` over the real handler) and asserts
   the invariant — Node interleaves at every `await`, so single-threaded code still races.
5. **Verify, then stop.** Done when: the type check, lint and format check are clean with
   no new suppressions, tests (with the repo's coverage gate) are green, and every AC has
   a test. Record the output with the `record-evidence` skill; the runner submits the work.

## Commands

- Never install: `node_modules` in a delivery worktree is shared with the main checkout,
  and `npm ci`/`pnpm install`/`yarn install` are hook-blocked (the `dependency-upgrade`
  skill). Run the tools already installed. Headless, plain `npx x` downloads a missing
  package without asking (a missing `tsc` fetches an unrelated npm package), so always
  write `npx --no -- x`, which fails instead of fetching (keep the `--`: `npx --no x`
  misreads the tool name).
- Types: `npx --no -- tsc --noEmit -p .` (or `npx --no -- tsc -b` for project
  references, or the repo's `typecheck` script).
- Lint and format: the repo's `lint` script (ESLint with typescript-eslint, or
  `npx --no -- biome check`) and `npx --no -- prettier --check .`.
- Tests: the repo's `test` script (Vitest, Jest, `node --test`); one file:
  `npx --no -- vitest run path/x.test.ts` or `npx --no -- jest path/x.test.ts`.

## Idioms that matter

- **Strict stays strict**: never loosen `tsconfig` or ESLint to pass, never add
  `ignoreDeprecations`. `@ts-expect-error` with a reason beats `@ts-ignore`; both need one.
- **`unknown` over `any`** at every boundary; `catch (e)` is `unknown` — narrow it.
- **Validate, don't cast**: `await res.json() as User` and `JSON.parse(x) as T` are lies
  the compiler believes. Parse with the repo's schema library (Zod, Valibot, TypeBox) and
  derive the type from the schema.
- **Model precisely**: discriminated unions with an exhaustive `switch` (a `never` check
  in `default`); `satisfies` over `as`; no non-null `!` on values that can be absent;
  `readonly` and `as const` for data that must not change.
- **Modules**: named exports; `import type` for type-only imports (required under
  `verbatimModuleSyntax`); the repo's ESM/CJS style and path aliases; `const` over `let`,
  never `var`.
- **typescript-eslint** type-aware rules worth honouring even when not configured:
  `no-floating-promises`, `no-misused-promises`, `await-thenable`,
  `switch-exhaustiveness-check`, `no-unsafe-*`, `only-throw-error`.
- **Pitfalls**: `===` only; `arr.sort()` mutates and sorts numbers as strings (use
  `toSorted((a, b) => a - b)`); `for…in` iterates keys, not elements; money is not a float.

## Concurrency and resource safety

- **Every promise is awaited, returned, or explicitly handled** (`void p.catch(log)`).
  `array.forEach(async …)` does not wait; use `for…of` with `await` or `Promise.all`.
- **Bound fan-out**: `Promise.all` over user-sized input needs a concurrency limit
  (the repo's limiter, e.g. `p-limit`, or a worker queue); use `Promise.allSettled` when one failure must not drop
  the others' results.
- **Read-modify-write across `await`** races between concurrent requests (read, await,
  write). Serialise per key (a promise-chain mutex or queue), or do it in one database
  transaction or atomic update with a version check.
- **Files other requests read**: `fs.writeFile` is not atomic. Write to a unique temp path
  in the same directory (`${target}.${crypto.randomUUID()}.tmp`), then `fs.rename` over the
  target. Never a fixed temp name or one built from `Date.now()`. Cross-process exclusion
  uses the database or a real lock; a time-only lease lets a paused holder write late, so
  writes must check a fencing token or version.
- **`fetch`** resolves on 4xx/5xx — check `res.ok`; it has no default timeout — pass
  `signal: AbortSignal.timeout(ms)`. Propagate `AbortSignal` through long operations.
- **Streams** use `pipeline` from `node:stream/promises` so errors and cleanup propagate;
  file handles opened with `fs.promises.open` are closed in `finally`.
- **Timers and listeners** are cleared/removed when their owner ends; module-level
  mutable state in a server is shared by every request.

## Review checklist — flag as defects

Walk this against the diff. An item is grounds for CHANGES only when, in changed code, it
causes a concrete failure (wrong result, crash, lost or corrupted data, security hole) or
leaves an AC's own behaviour untested: cite the line and that failure. Otherwise it is an
`(optional)` note. Formatting the tools would fix, and preferences the repo
does not enforce, are not findings. Do not patch the code under review.

- [ ] `tsconfig`/ESLint loosened; a new `any`, `@ts-ignore`, `eslint-disable` or `!`
      without a reason.
- [ ] External data (request body, `JSON.parse`, `res.json()`, env) cast with `as` instead
      of validated.
- [ ] A floating promise; `forEach(async …)`; an async callback passed where a sync one is
      expected; an empty `catch`.
- [ ] Unbounded `Promise.all` over user-controlled input.
- [ ] A read-modify-write spanning an `await` with no per-key serialisation, transaction
      or version check; shared module-level state mutated per request.
- [ ] A shared file written in place or via a fixed or time-based temp name; a time-only
      lock lease.
- [ ] `fetch` without an `ok` check or a timeout; a stream piped without error handling; a
      timer, listener or handle never released.
- [ ] A `switch` over a union without exhaustiveness where a new member would be missed.
- [ ] An AC has no test, or the test mocks the module the AC is about.

## Capture lore

This skill is one of the places durable, reusable knowledge naturally surfaces:
**A TypeScript/stack convention this repo enforces beyond the obvious — an import-ordering rule, a type-modelling pattern, or a lint/tsconfig constraint.** That kind of fact is *lore*. Capture it via the **lore-capture
protocol in your brief** (`CLAUDE.factory.md`, step 11 "Memory contribution"):
call the Memory MCP `suggest_lore` once at the close of your work — reusable
conventions, gotchas, decisions, and boundaries only, never per-ticket trivia.

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…