--- name: js-typescript description: >- 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`](skills-public/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 ```jsonc { "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 `