Core TypeScript Conventions — Base Engineering Skill
The language-level TypeScript rules for every project, framework or not. react, angular, and vue
extend it; architecture-and-design composes with it. Every rule assumes the compiler runs in
strict mode (compiler-config). Examples are plain TypeScript, no framework.
Builds on. Nothing — this is a base skill.
This SKILL.md is self-sufficient: the Ruleset below is the complete, enforceable list. Each
references/<topic>.md holds that group's reasoning and ❌ / ✅ code, and
references/worked-example.md a full review pass; open them for depth when your runtime allows.
How to Use This Skill
Pick the mode that matches the task. Do the steps in order.
| Mode | Steps |
|---|---|
| Generate — write new TypeScript | 1. Assume strict is on (compiler-config). 2. Write the code, applying the Ruleset as you go. 3. Annotate the return type of every exported function (functions). 4. Run the Ruleset as a checklist. Fix each fail before you hand off. |
| Review — check a diff | 1. Run the Ruleset against the diff. 2. Write one finding per fail, in the Output Format below. 3. Order the findings: must-fix first, then consider. 4. If nothing fails, say so in one line. Do not invent findings. |
Configure — set up or audit tsconfig.json and ESLint |
1. Start from assets/tsconfig.base.json and assets/eslint.config.js. 2. Turn on every flag in compiler-config and every rule in lint. 3. For a flag you cannot turn on yet, add a // TODO with the reason. 4. Do not relax a flag or disable a rule to clear an error. Fix the code. |
Output Format
Write one finding per line:
<severity> · <topic> · <file>:<line> — <what is wrong>. <the fix as an action>.
<severity>ismust-fix(breaks a rule in this skill or the build) orconsider(safe, but a rule prefers another form).<topic>is a Ruleset topic slug (unsafe-types,narrowing,async, …).
Rules for Every Mode
- Name the Ruleset topic when you enforce a rule.
- Prefer the simplest type that stays precise. A wider type hides bugs; an over-clever type slows the next reader.
- A type that needs a comment to explain it is a smell. Give it a name, or make it simpler.
Ruleset
compiler-config → references/compiler-config.md
-
strictis on, plusnoUncheckedIndexedAccess,exactOptionalPropertyTypes,noImplicitOverride,noFallthroughCasesInSwitch,noImplicitReturns,noUnusedLocals,noUnusedParameters,verbatimModuleSyntax, anderasableSyntaxOnly. - A flag is never relaxed to clear an error — the code is fixed instead.
unsafe-types → references/unsafe-types.md
- No
any: an unknown input is typedunknownand narrowed. Noas any, noas unknown as T. - No
asto force a type, and no!, unless the value is proven present on the line above — each such use has a guard or a comment that proves it safe. - No
Function,Object, or{}as a type (objectfor a non-primitive is fine). - No
@ts-ignore—@ts-expect-errorwith a comment.
inference → references/inference.md
- No annotation that only repeats what the compiler already infers exactly (exception: an exported API boundary — see
functions). -
satisfieschecks a literal against a shape without widening the value's type.
data-modeling → references/data-modeling.md
- A string set is a union of string literals or a frozen
as constobject — never a TypeScriptenumorconst enum. - Unchanging data is
readonly(readonly T[]for a list). - Mutually exclusive states are a discriminated union with a shared discriminant field. Design rationale:
architecture-and-design, patterns. - A domain id has its own branded type, minted with one sanctioned cast at the boundary. Design rationale:
architecture-and-design, type-safety.
narrowing → references/narrowing.md
-
unknownand unions are narrowed with a type guard (v is T) or an assertion function (asserts v is T), not a cast. - Every
switchon a union ends with anassertNeverdefault.
generics → references/generics.md
- A type parameter is added only when two types move together (an input and a return type); no single-use type parameter.
- A type parameter is constrained with
extends; a common one has a default; aconsttype parameter where the caller passes a literal.
utility-types → references/utility-types.md
- A related type is derived with
Pick/Omit/Partial/Required/Record/ReturnType/Parameters/Awaited/NonNullable/keyof/typeof/ indexed access / a template literal type — not hand-copied.
nullability → references/nullability.md
-
undefinedis the one empty value;nullonly for an external contract that sends it. -
array[i]andrecord[key]are read as possiblyundefined(needsnoUncheckedIndexedAccess). -
?.and??for a default —||only where0,'', andfalseare not valid values.
functions → references/functions.md
- Every exported function annotates its return type; a local, non-exported function infers it.
- One options object past three parameters — no long list of order-dependent, especially boolean, arguments.
- A union parameter over an overload set when the bodies match.
async → references/async.md
- No floating promise — every promise is awaited or explicitly marked
void. - Independent async work runs with
Promise.all; serialawaitonly for a step that needs the previous result. - A disposable resource (a timer, subscription, lock, or handle) is released with
using/await usingwhere the target supports explicit resource management.
errors → references/errors.md
- A
catchbinding isunknown(fromuseUnknownInCatchVariables) and is narrowed before any field read. - A thrown value is always an
Error— a custom type extendsErrorand setsname; never a string or a plain object.
modules → references/modules.md
- A type reference uses
import type/export typeor an inlinetypeon the name. - Named exports over a default export; no circular import between two modules; no project-wide barrel (a feature
index.tsis fine).
language-hygiene → references/language-hygiene.md
-
===/!==only — the one exception isx == null, which testsnullandundefinedtogether. -
Number.isNaN/Number.isFinite, not the coercing globals. -
constby default,letonly when reassigned, nevervar; a function parameter is not reassigned. - Real bit flags are a
constobject of powers of two plus a derived type, not a string-literal union.
lint → references/lint.md
-
typescript-eslintruns withstrict-type-checked(or at leastrecommended-type-checked), withno-floating-promises,no-explicit-any,consistent-type-imports, andswitch-exhaustiveness-checkon. - Prettier formats, so style is not a review topic.
Limits
This skill is language-level TypeScript. It does not cover:
- Architecture, layering, state management, and component design — see
architecture-and-design. - Framework APIs and patterns — see
react,angular, andvue. - Build tooling, bundlers, and monorepo setup beyond the
tsconfigbaseline incompiler-config. - Runtime schema libraries (Zod, Valibot) — named where relevant, not taught here.
This skill decides the syntax. It does not replace reading the code and understanding the domain.
References
This skill composes with:
architecture-and-design— the design layer. On a shared topic (discriminated unions, branded ids) it decides the design and this skill decides the syntax.react/angular/vue— extend this skill with their framework's specifics. On a language-point conflict, this skill wins.