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
- TypeScript — always the default for new code.
- JavaScript with JSDoc type annotations — acceptable when a project genuinely cannot
adopt a build step, and
checkJsis enabled so the annotations are actually enforced. - 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
noImplicitAnyornoUncheckedIndexedAccessbecause their idioms depend on it. When contributing upstream, respect the project'stsconfig.jsonas-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 Toveras Twhen the value already conforms.asis for genuine narrowing only — it is an assertion that silences the checker, not a conversion.- Zero
anyin 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, neverundefinedorfalse. - 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/useCallbackon 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.Fragmentonly when the fragment needs akey. - Type props with a named
interface, notReact.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
addEventListenerneeds a named function reference. An inline arrow passed toaddEventListenercannot be matched byremoveEventListener, 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'smax-linesandmax-lines-per-functioncover both natively, so prefer those over a separate test here.
8. Async & Data
async/awaitover 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.allfor independent work; sequentialawaitonly 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, orelectrondirectly. 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 assertstartsWith(<allowed root>)before any read or write. - Subprocesses:
spawnwith an argv array. Neverexeca constructed string, nevershell: truewith untrusted input. - Network: allowlist by origin, cap response size, cap redirect depth.
- Errors crossing the boundary must be scrubbed — a raw
Errorleaks 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 followbash.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.mjsand 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.