TypeScript code in projects using this skill targets strict-by-default
type-checking — tsc --strict, ESLint with type-aware rules such as
strictTypeChecked, and Zod for runtime validation at boundaries. The bar is
not chosen for aesthetic reasons, but because TypeScript's type system buys
nothing when any is allowed to leak: every escape becomes a corner the
compiler stops checking, and the type system silently degrades from a
verification tool into a documentation overlay. The rules below state what is
load-bearing about that bar; the canonical examples in
references/canonical-examples.md show the accepted shapes.
This skill is rules, not a tutorial. It assumes familiarity with generics,
discriminated unions, mapped types, conditional types, and template literal
types — what it specifies is which of the available patterns projects using
this skill have chosen, and which they have rejected.
-
Version-specific claims must be source-checked. Before changing guidance
tied to a TypeScript version, query Context7 or the official TypeScript
release notes / handbook. Do not rely on memory for compiler defaults,
deprecations, or narrowing behavior.
-
Treat TS 6.0 as the baseline, not a temporary migration target. New code
should satisfy TS 6.0 without ignoreDeprecations: "6.0". That setting is
allowed only as a short-lived migration marker with an inline comment or
linked issue naming the deprecated option and removal plan.
-
Compiler defaults are explicit in shared configs. TS 6.0 defaults
strict to true, module to esnext, and target to the current-year
ECMAScript version. Still write these values explicitly in reusable
tsconfig bases so downstream packages do not depend on moving defaults.
Direct Node.js packages use module: "nodenext"; bundled apps normally keep
module: "esnext" with moduleResolution: "bundler".
-
Global ambient types are explicit. TS 6.0 defaults compilerOptions.types
to []. Add only the globals a project needs, e.g. ["node"],
["vitest/globals"], or ["jest"]. types: ["*"] restores the old
node_modules-wide scan and is rejected unless it is a documented migration
escape hatch.
-
Set rootDir deliberately for emitting projects. TS 6.0 defaults
rootDir to the directory containing tsconfig.json. Libraries and apps
that emit to dist should set rootDir explicitly, usually "./src", so
output paths do not change when files move.
-
Do not use baseUrl as a lookup root. TS 6.0 deprecates baseUrl.
Path aliases include their full prefix directly:
"@app/*": ["./src/app/*"], not "baseUrl": "./src" plus
"@app/*": ["app/*"]. A catch-all "*": ["./src/*"] is permitted only
when intentionally preserving old lookup-root behavior during migration.
-
Deprecated compiler options are not project policy. New configs must not
use target: "es5", module: "amd" | "umd" | "system" | "systemjs" | "none",
moduleResolution: "classic" | "node" | "node10", outFile,
downlevelIteration, alwaysStrict, suppressImplicitAnyIndexErrors, or
keyofStringsOnly. Do not set esModuleInterop or
allowSyntheticDefaultImports to false.
-
Import attributes use with, not assert. JSON and other attribute
imports use import data from "./data.json" with { type: "json" }.
import ... assert { ... } and dynamic import assert options are rejected
in new code.
-
Command-line file checks must not bypass project config. In a directory
with tsconfig.json, use tsc --noEmit -p tsconfig.json or the project
script. Do not run tsc src/file.ts expecting project options to apply.
-
DOM iterable libs are folded into dom. New browser configs should not
add dom.iterable or dom.asynciterable out of habit; include only the
libs the project actually needs.
-
Standard decorators (TS 5.0) — not experimentalDecorators. TypeScript
5.0 shipped decorators per the TC39 Stage 3 proposal. Do not enable
experimentalDecorators in new code; the two systems are incompatible.
Standard decorators receive typed context objects (ClassMethodDecoratorContext,
ClassFieldDecoratorContext, ClassDecoratorContext, etc.) alongside the
decorated value. Decorator functions return void or a replacement value
compatible with the decorated declaration. Remove emitDecoratorMetadata alongside
experimentalDecorators when migrating.
-
using declarations (TS 5.2) — prefer over manual try/finally cleanup.
using resource = acquire() calls resource[Symbol.dispose]() at end of
scope, whether the scope exits normally or via exception. await using does
the same for async cleanup via Symbol.asyncDispose. Implement Disposable
or AsyncDisposable for any resource that has a teardown step. This replaces
the try { … } finally { resource.close(); } pattern; do not write new
try/finally teardown blocks when using applies.
-
Inferred type predicates — explicit x is T is now often unnecessary.
TypeScript infers x is T return types for functions whose body is a
narrowing expression. Write the predicate without the annotation first; add
an explicit x is T return annotation only when inference produces the wrong
type or when the predicate is part of a public API that must be stable across
refactors. Do not write explicit predicates for documentation — they drift.
-
NoInfer<T> utility (TS 5.4) — block unwanted inference sites. Wrap a
type parameter occurrence in NoInfer<T> to prevent that position from
contributing to inference. Canonical use:
function fallback<T>(value: T | undefined, def: NoInfer<T>): T. Without
NoInfer, the def argument can widen the inferred T unexpectedly.
-
infer T extends SomeType constraints (TS 4.7+) — bound inferred types.
infer T extends string infers T and narrows it to string in the true
branch, eliminating a secondary T extends string ? guard. Use it to avoid
double conditional nesting in template-literal and mapped-type helpers.
-
Awaited<T> for async return types. Use Awaited<ReturnType<typeof fn>>
rather than manually unwrapping Promise<T> levels. Awaited handles nested
and thenable types correctly. Annotate the function's own return type
directly; use Awaited in generic utility types that operate over async
functions.
Long-form supporting material is split out so the rules above stay
scannable on first read: