TypeScript
Purpose
Provide portable TypeScript defaults that keep types honest, runtime boundaries explicit, and code readable under strict settings.
When to use this skill
- Writing or refactoring TypeScript modules or scripts.
- Deciding how to model states, responses, and shared types.
- Reviewing TypeScript config and type-checking strictness.
- Handling unknown external data safely.
Scope Boundaries
- Use this skill for strict TypeScript design, runtime boundaries, and package-level structure.
- Use
ref-sp-js-reactwhen the main question is about React component structure, hooks, client-side state ownership, or React-specific dependency choices. - Use
ref-sp-js-nextwhen the main question is about Next.js framework structure, App Router, or Next-specific integrations. - Use
ref-sp-js-javascriptwhen the code intentionally stays in plain JavaScript with JSDoc rather than full TypeScript. - Use
ref-sp-dev-coding-patternsfor language-agnostic naming, comments, CLI ergonomics, and testing defaults. - Use
ref-sp-dev-projects-architecturefor generic feature-boundary or shared-utility decisions that are not TypeScript-specific.
Defaults
- Prefer strict mode and keep it strict.
- Prefer
unknownplus narrowing overany. - Prefer discriminated unions for stateful variants.
- Prefer explicit runtime validation at trust boundaries.
- Prefer inferred function return types by default when TypeScript can express the result cleanly from the implementation.
- Add explicit return annotations when the function defines a public API contract, implements an interface or overload, is recursive or generic enough to infer poorly, returns a deliberately narrow union, or crosses a framework/library boundary where the contract matters more than the implementation.
- Prefer
as constfor fixed literal maps and tuples when the exact keys or values matter; do not widen them toRecord<string, ...>orstring[]unless the surface is intentionally open-ended. - Prefer const data as the source of truth for closed sets: derive key and value unions from
as constobjects or tuples instead of maintaining a parallel hand-written type that can drift. - Prefer TypeScript over plain JavaScript in modern Node and Deno codebases because current runtimes can execute
.tsand.mtsdirectly. - Modern Node can run TypeScript directly through built-in type stripping; do not add
ts-node,tsx, or a build step just to execute ordinary Node-owned.tsor.mtsscripts. - Prefer
.tsfor ordinary TypeScript modules, colocated feature tests, and most feature code. - Prefer
.mtsfor Node ESM scripts that are executed directly by Node and need the extension to communicate module format clearly. - Prefer
constarrow functions for TypeScript helpers, callbacks, components, and script-local functions. - Prefer Yarn for dependency management and script execution in Node-based TypeScript projects unless the repo is intentionally Deno-owned.
Task Framing
| Command or action | What | Why | When | Expected outcome |
|---|---|---|---|---|
| Model a runtime boundary | Decide where external data becomes validated domain data. | TypeScript is safest when compile-time certainty matches runtime reality. | When reading network, file, env, or parsed JSON input. | The code narrows or validates untrusted input before use. |
| Place types and features | Keep package-owned code and its local types under a readable src/<feature>/ layout. |
Deeply shared types/ folders often grow faster than the actual features they serve. |
When starting or reorganizing a package. | The feature and the types it owns are easy to find together. |
| Review strictness and readability | Check whether utility types, generics, and unions are helping or obscuring the model. | Strictness loses value when the types stop communicating intent. | When reviewing or refactoring a non-trivial TypeScript module. | Runtime safety stays strong without burying the domain in type tricks. |
Core Rules
Type design
- Model states and variants with unions instead of optional-property soup.
- Use utility types sparingly and only when they clarify intent.
- Keep literal lookup tables precise with
as const, then narrow dynamic keys withkeyof typeof ...or a guard instead of throwing away the literal information. - When a fixed lookup object already defines the allowed states, labels, or variants, derive unions from it instead of duplicating the same domain in a separate alias or enum.
- Small helpers such as
type Keys<T extends object> = keyof Tandtype Values<T extends object> = T[Keys<T>]are fine when they make those derived unions easier to read and reuse. - Avoid deep type-level cleverness when a simple domain type would read better.
Const-first modeling
- Prefer this pattern for closed maps and label sets:
export const statValueLabels = {
1: 'Basso',
2: 'Medio',
3: 'Alto',
} as const;
export type Keys<T extends object> = keyof T;
export type Values<T extends object> = T[Keys<T>];
export type StatValue = Keys<typeof statValueLabels>;
export type StatValueLabel = Values<typeof statValueLabels>;
- Use an explicit
Record<...>annotation only when the object is intentionally open-ended or must satisfy a broader external contract.
Runtime boundaries
- Treat network data, filesystem data, environment variables, and parsed JSON as untrusted until validated.
- Narrow
unknownwith guards or validation helpers before use. - Do not let compile-time confidence hide missing runtime checks.
Direct Node execution
- Use direct Node TypeScript execution for Node-owned scripts when the supported Node version includes built-in type stripping; older Node 22 releases may need
--experimental-strip-types. - Keep directly executed TypeScript erasable: avoid syntax that requires transformation, such as enums, namespaces with runtime output, or parameter properties, unless the repo intentionally enables a transform path.
- Direct execution is not type checking. Keep
tsc --noEmit, editor checks, or another explicit checker when correctness depends on TypeScript diagnostics. - Do not ship directly executed
.tsor.mtsfiles as no-build package runtime fromnode_modules; Node rejects type stripping there, so publish JavaScript or use.mjsplus JSDoc/checkJs when no build exists.
Code structure
- Keep module top level free of side effects: importing a module should bind names, not open connections, read environment, or construct stateful clients. Export a factory such as
export const createDb = () => connect(env.databaseUrl)instead of a ready-made instance, so imports stay order-independent and tree-shaking and test isolation keep working. - Construction at an application entry point or composition root is the intended exception; the rule targets work triggered by importing an ordinary module. See
ref-sp-dev-coding-patternsfor the portable rule. - Keep types close to the feature or module that owns them.
- Extract shared types only when they are truly shared.
- Prefer readable named
constarrow functions and objects over dense callback chains. - Use function declarations only when the declaration form is materially useful, such as overload implementations, generators, intentional hoisting, or matching an existing framework/API convention.
- Let ordinary helper return types be inferred; annotate return types when they communicate an API boundary or prevent accidental widening.
File and config conventions
- Use
.tsfor package code, browser-oriented modules, and most feature files. - Use
.mtsfor repo scripts or Node-run ESM entrypoints when the runtime or surrounding repo conventions rely on explicit module extensions. - Use
.d.tsfor ambient declarations or globals that support the main code, such as userscript global definitions. - Split
tsconfigfiles by runtime surface when a repo mixes Node scripts, browser modules, and userscripts instead of forcing one project file to describe incompatible environments. - Prefer Jest when a Node-managed package needs one test runner for colocated JavaScript and TypeScript feature tests.
Example Layouts
TypeScript package in a monorepo
packages/package-name/
src/
invoice-import/
index.ts
parse-csv.ts
parse-csv.test.ts
types.ts
Small feature with local validation
src/
session-state/
index.ts
validate-session.ts
validate-session.test.ts
Node ESM scripts with a dedicated TypeScript project
scripts/
generate-catalog.mts
refresh-manifest.mts
tsconfig.json
Gotchas
- Strict compile-time checks do not replace runtime validation for external data.
unknownis only safer thananyif the code actually narrows it before use.- Large type-level abstractions can hide the domain model instead of clarifying it.
- Parallel literal types and literal objects drift unless one becomes the clear source of truth; prefer the const object and derive from it.
- Node's TypeScript execution strips types; it does not replace a checker or make non-erasable TypeScript syntax safe in every runtime mode.
- A side effect at module top level — a
connect(...)ornew Client(...)bound at import — breakssideEffects: falsetree-shaking and makes import order significant; defer it to an exported factory. - No-build package runtime under
node_modulesis the important exception: use emitted JavaScript or JSDoc-backed.mjsthere.
Validation
- No new
anyescapes or unchecked casts were introduced without strong justification. - External input is validated before domain logic uses it.
- Shared types are easy to locate and easy to understand.
- Module imports have no side effects; connections and stateful clients are built by exported factories, not at import time.
- Functions follow the local const-arrow default unless overloads, generators, intentional hoisting, or an API convention justify a declaration.
- The code does not stay on plain JavaScript merely to avoid
tsc,ts-node, or a build step that modern Node and Deno runtimes no longer need. - The resulting code remains readable to someone who did not write the types.
References
- TypeScript Handbook: https://www.typescriptlang.org/docs/
- TSConfig Reference: https://www.typescriptlang.org/tsconfig
- Read
./references/checklist.mdfor a quick strict-TypeScript review pass. - Read
./references/config-templates.mdwhen you need ready-to-adapttsconfigtemplates derived from theweb-pagesrepository. - Read
./assets/trigger-eval-queries.example.jsonwhen testing trigger quality for TypeScript requests.