Stack conventions
TypeScript
TypeScript 5.x applications and libraries with strict compiler settings, on Node.js or in the browser.
TypeScript 5.x applications and libraries with strict compiler settings, on Node.js or in the browser.
Used for: typescript.
Principles
- Keep the compiler in strict mode and let types describe the real shape of data.
- Never use any in new code; use unknown and narrow it, or write the precise type.
- Validate data that crosses a trust boundary (HTTP, storage, environment) at runtime before giving it a static type.
- Model states with discriminated unions so impossible states cannot be represented.
- Prefer small pure functions and immutable data; isolate side effects in clearly named modules.
- Handle every promise; an unawaited promise is a lost error.
- Export a small, explicit public surface from each module.
- Write tests with the change that check behaviour through the public API.
- Avoid type assertions (as); check a value against a type with satisfies and narrow with type guards, so the compiler verifies rather than trusts.
- Let inference type locals, but annotate exported functions' parameters and return types so module contracts are explicit and stable.
Conventions
Accessibility
- When the change renders UI, use semantic HTML elements and accessible names for controls.
Concurrency
- Use async and await consistently; do not mix callbacks and promises in one flow.
- Bound parallel requests to external services and handle partial failure with Promise.allSettled where appropriate.
- Pass AbortSignal to long-running operations so callers can cancel them.
Dependencies
- Add packages with the repository's package manager and commit its lock file.
- Prefer packages that ship their own types; add @types packages as dev dependencies only.
- Do not add a dependency for a few lines of standard code.
Documentation
- Document exported functions and types with TSDoc comments that state behaviour, parameters and thrown errors.
- Let types document shapes; comment the reasons behind non-obvious decisions.
- Update the README or docs when commands, configuration or public APIs change.
Errors
- Throw Error objects (or subclasses), never strings or plain objects.
- Narrow caught values from unknown before reading their properties.
- Await or explicitly handle every promise; do not leave floating promises.
- Add context when re-throwing by using the cause option of Error.
Logging
- Use the repository's logger rather than console in application code.
- Log structured fields (ids, counts, durations) rather than whole objects.
- Never log tokens, passwords, cookies or personal data.
Naming
- Use camelCase for variables and functions, PascalCase for types, classes and enums, and UPPER_SNAKE_CASE for true constants.
- Do not prefix interfaces with I or types with T.
- Name files the way the repository already does (kebab-case or camelCase), consistently.
Performance
- Avoid blocking the event loop with synchronous I/O or heavy CPU work in request handlers.
- Run independent async operations concurrently with Promise.all, with a bound on concurrency.
- Avoid repeated work inside loops and render paths; memoise only after measuring.
Security
- Validate and limit all request input with a schema before use.
- Never pass user input to eval, new Function, child_process with a shell, or dynamic require.
- Use parameterised queries or the ORM's query builder for databases.
- Read secrets from environment or a secret store and never ship them to browser bundles.
- Escape or sanitise data rendered as HTML; prefer the framework's safe rendering.
Structure
- Keep one responsibility per module and export only what other modules need.
- Use import type for imports used only as types.
- Prefer named exports over default exports unless the framework requires a default.
- Follow the repository's path aliases and module system (ESM or CommonJS); do not mix them.
Testing
- Use the repository's test runner (for example Vitest or Jest) and its file naming.
- Test behaviour through public functions; avoid testing private details.
- Cover error branches and every variant of a discriminated union.
- Keep tests deterministic with fake timers and injected clocks instead of real waiting.
- Write unit tests for pure logic and integration tests for HTTP handlers, data access and other boundaries, with real dependencies in containers or the framework's test server where the change touches them.
- Name test files after the module under test (
cart.test.ts) and test titles after the behaviour. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Does the change compile under the repository's strict compiler settings without new any or ts-ignore?
- Is data from outside the program validated at runtime before it is trusted?
- Is every promise awaited, returned or explicitly handled?
- Are errors thrown as Error objects and caught values narrowed from unknown?
- Are impossible states prevented by the types (for example with discriminated unions)?
- Does the change follow the repository's module system, naming and formatting?
- Is user input kept away from eval, shells, dynamic imports and raw SQL?
- Are secrets kept out of code, logs and browser bundles?
- Do new behaviours have tests, including error branches?
- Are exported functions and types documented where their behaviour is not obvious?
- Are new dependencies necessary, typed and recorded in the lock file?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
tsc |
typecheck | tsc | no |
eslint |
lint | eslint | no |
biome |
lint | biome | no |
prettier |
format | prettier | no |
npm-audit |
security | npm audit | no |
gitleaks |
security | gitleaks | no |