Files

9.0 KiB

name, description
name description
js-typescript Use this skill when writing, building, linting, testing, or releasing JavaScript or TypeScript — including Node, Bun, Deno, React, Next.js, and Astro projects. Covers the TypeScript-over-JSDoc-over-JS preference, strict compiler settings, banned constructs, React/hooks rules, resource lifecycle, and package/tooling choices.

TypeScript / JavaScript — Rules & Preferences

Extends coding-dev.md. Everything there applies; this adds JS/TS specifics.


1. Language Choice — A Strict Preference Order

  1. TypeScript — always the default for new code.
  2. JavaScript with JSDoc type annotations — acceptable when a project genuinely cannot adopt a build step, and checkJs is enabled so the annotations are actually enforced.
  3. Plain untyped JavaScript — last resort only.

Migrating an existing JS project to TS is worth proposing; do not do it unasked (scope rule). In a plain-JS codebase, match it — do not introduce a lone .ts file and a build step into a project that has neither.


2. Strict Compiler Settings

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true
  }
}

tsc --noEmit must pass with zero errors and zero warnings, and it belongs in the build gate alongside lint and tests — not as a thing someone runs occasionally in their editor.

Note

Some established codebases deliberately relax noImplicitAny or noUncheckedIndexedAccess because their idioms depend on it. When contributing upstream, respect the project's tsconfig.json as-is. In our own repos and non-upstreamed forks, the settings above are the target — tightening them is fair game (as its own change, not smuggled into a feature).


3. Banned Constructs

Never Use instead
any unknown + narrowing, or a real generic
as unknown as T a type guard, or fix the source type
@ts-ignore @ts-expect-error with a one-line reason, only when unavoidable
! non-null assertion narrow with if or an early return
enum / const enum as const object + literal union
namespace ES modules
Boxed primitives (Number, String, Object, {}, Function) the primitive types
../../.. relative climbs the configured path alias (@/*)
JSON.parse(JSON.stringify(x)) structuredClone or lodash.cloneDeep
await new Promise(r => setTimeout(r, ms)) a sleep helper from your utils
console.log in shipped code a real logger with levels
Empty catch {} handle it, or let it propagate
  • satisfies T over as T when the value already conforms. as is for genuine narrowing only — it is an assertion that silences the checker, not a conversion.
  • Zero any in shared model types. Frontend types that mirror a backend schema must stay structurally in sync with it and fully typed.

4. Use What Exists

Search the codebase's own utils/, common/, and shared modules before writing anything. Re-implementing an export that already exists is the single most common avoidable diff.

Never hand-roll: debounce/throttle (lodash), deep clone, clamp, date formatting (date-fns / dayjs / project standard), URL parsing, regex escaping, UUID generation, retry/backoff, or a semver comparator.

In a UI framework or client mod, never recreate a component the host already provides — a hand-rolled <button> loses theming, focus rings, and accessibility.


5. React

  • Conditional rendering returns null, never undefined or false.
  • List keys are stable identifiers. Never key={index} on a mutable list.
  • Don't mirror external state into useState. Subscribe to the store with its own hook.
  • No useMemo / useCallback on cheap values that do not feed a dependency array. They are not free, and they obscure what is actually expensive.
  • Hoist constant dependency arrays to module scope (const KEYS = [...] as const). A fresh array literal in the render body re-subscribes on every render.
  • Prefer <>…</> fragments; React.Fragment only when the fragment needs a key.
  • Type props with a named interface, not React.FC.
  • Wrap anything injected into a host application's tree in an error boundary — an uncaught throw in injected code takes down the host's render, not just yours.
  • Components used as dynamic render targets must live at module scope. Defining a component inside another component remounts its whole subtree every render.

6. Resource Lifecycle — Everything Started Must Be Stopped

Every setInterval, long setTimeout, addEventListener, subscription, ResizeObserver, IntersectionObserver, AbortController, and WebSocket created in a setup path must be torn down in the matching teardown path — stop(), useEffect cleanup, or finally.

Caution

addEventListener needs a named function reference. An inline arrow passed to addEventListener cannot be matched by removeEventListener, so the listener leaks silently and accumulates across every mount.

let intervalId: ReturnType<typeof setInterval> | undefined;

function start() { intervalId = setInterval(tick, 30_000); }
function stop()  { if (intervalId !== undefined) { clearInterval(intervalId); intervalId = undefined; } }

Attach an AbortController to every long-lived fetch and abort it in cleanup.


7. Structure & Naming

  • Components: PascalCase .tsx.
  • Modules / utilities: kebab-case .ts.
  • Imports: prefer the configured alias (@/…) over relative climbs.
  • Indentation and quote/semicolon style: match the file you are editing. Do not introduce a new formatter or lint config into a project that already has one.
  • One file until it earns a split — when it nears the 1000-line limit, or when a second file is genuinely reusable (utils.ts, types.ts, components/). The limits from the general rules (1000 lines per file, 200 per function) apply: ESLint's max-lines and max-lines-per-function cover both natively, so prefer those over a separate test here.

8. Async & Data

  • async/await over raw .then() chains. No mixing both in one flow.
  • Never leave a promise unawaited unless you explicitly mean to, and mark it (void p) when you do.
  • Promise.all for independent work; sequential await only when there is a real dependency.
  • Subscribe to events; never poll. (Global rule — it applies with full force here.)
  • Never store secrets in localStorage / sessionStorage. Use the platform's real storage API, and namespace your keys.

9. Node / Runtime Boundaries

  • In any sandboxed or privileged-split architecture (Electron, browser extensions, plugin hosts), the untrusted side must never import fs, path, child_process, net, http, or electron directly. Route through the designated IPC/native boundary.
  • Every IPC handler argument is untrusted — validate type, length, and content.
  • Filesystem paths from the other side: normalize() then assert startsWith(<allowed root>) before any read or write.
  • Subprocesses: spawn with an argv array. Never exec a constructed string, never shell: true with untrusted input.
  • Network: allowlist by origin, cap response size, cap redirect depth.
  • Errors crossing the boundary must be scrubbed — a raw Error leaks absolute paths.

10. Tooling & Tests

  • Respect the project's runtime and package manager (npm / pnpm / yarn / bun) and its lockfile. Do not switch package managers or add a second lockfile. Build and release scripts around it follow bash.md.
  • Test runner: whatever the project uses (vitest, bun test, jest). Tests live beside the code as *.test.ts(x) unless the project says otherwise.
  • Add unit tests for new logic and route tests for new handlers. Prioritize coverage on auth, persistence, and anything that talks to a third party.
  • Lint with the project's config (eslint.config.mjs and friends). Do not add a new lint or format config to a repo that already has one.
  • Know the project's expected test baseline. Some inherited suites are not all-green on a clean checkout; judge regressions against the documented baseline rather than a raw run — and if a baseline file exists, use it.

11. Framework Versions Move Fast

Warning

Next.js, Astro, React, and the major bundlers ship breaking changes regularly, and their current APIs frequently differ from what is in a model's training data. Read the version's own docs in node_modules/<pkg>/dist/docs/ or the project's pinned documentation before writing code against them. Heed deprecation notices rather than pattern-matching from memory.