Modern TypeScript 5.x idioms — strict tsconfig (strict, noUncheckedIndexedAccess), the type system (unions/intersections, generics + constraints, narrowing, discriminated unions, unknown vs any, utility types, satisfies, as const, template-literal types), ESM & moduleResolution, and type-safe patterns (branded types, exhaustiveness). Use when configuring a tsconfig, writing or reviewing type-safe TypeScript, designing generics or a discriminated union, or decoding a tsc error.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Modern TypeScript 5.x idioms — strict tsconfig (strict, noUncheckedIndexedAccess), the type system (unions/intersections, generics + constraints, narrowing, discriminated unions, unknown vs any, utility types, satisfies, as const, template-literal types), ESM & moduleResolution, and type-safe patterns (branded types, exhaustiveness). Use when configuring a tsconfig, writing or reviewing type-safe TypeScript, designing generics or a discriminated union, or decoding a tsc error.
license
MIT
metadata
{"version":"1.0","skill-author":"vault-audit"}
Modern TypeScript
Overview
Guidance for idiomatic TypeScript on current stable 5.x (as of 2026). The goal: let the
compiler prove things so you don't have to test them. Modern TS means strict on from day one,
letting inference do the work, reserving any for genuine escape hatches, and using unions +
narrowing instead of runtime type tags. TypeScript's type system is structural (shape-based,
not name-based) — internalizing that explains most surprises.
This skill covers the type system and compiler config. It is framework-agnostic: for React/Next
specifics see the frontend skills noted at the bottom.
Strict baseline tsconfig.json. Every option below is a real, documented 5.x flag — do not
substitute made-up names.
{"compilerOptions":{"target":"es2022","lib":["es2023"],// add "dom","dom.iterable" for browser code"module":"nodenext",// Node: nodenext | bundler apps: "preserve""moduleResolution":"nodenext",// bundlers (Vite/esbuild/webpack): "bundler""moduleDetection":"force",// treat every file as a module (no accidental globals)// Correctness — the important half"strict":true,// umbrella: noImplicitAny, strictNullChecks,// strictFunctionTypes, useUnknownInCatchVariables, ..."noUncheckedIndexedAccess":true,// arr[i] and record[k] become T | undefined"exactOptionalPropertyTypes":true,// `x?: T` cannot be explicitly set to undefined"noImplicitOverride":true,// require `override` keyword"noFallthroughCasesInSwitch":true,"noUnusedLocals":true,"noUnusedParameters":true,// Module hygiene"verbatimModuleSyntax":true,// forces `import type`; predictable emit, no elision"isolatedModules":true,// safe for per-file transpilers (esbuild/swc/babel)"esModuleInterop":true,"skipLibCheck":true,// skip checking .d.ts of deps (faster, standard)// Emit (drop if a bundler emits and you only type-check)"declaration":true,"sourceMap":true,"outDir":"dist","noEmit":true// remove when tsc is the emitter}}
Rules of thumb: Node/library → module+moduleResolution both "nodenext".
App built by a bundler → "module": "preserve", "moduleResolution": "bundler", "noEmit": true.
noUncheckedIndexedAccess is the single highest-value flag not included in strict — turn it on.
Core patterns
unknown, never any, at boundaries.any disables checking and spreads silently; unknown
forces a narrow before use.
constdata: unknown = JSON.parse(raw); // JSON.parse returns any — annotate to unknownif (typeof data === "object" && data !== null && "id"in data) {
// data narrowed; validate the rest with a schema lib (zod/valibot) for real safety
}
Narrowing via control-flow analysis — typeof, instanceof, in, truthiness, equality:
functionlen(x: string | string[]): number {
returntypeof x === "string" ? x.length : x.length; // each branch narrowed
}
Discriminated (tagged) unions — the workhorse for modeling states. A shared literal
kind/type field lets TS narrow the whole object:
typeResult<T> =
| { kind: "ok"; value: T }
| { kind: "err"; error: Error };
function unwrap<T>(r: Result<T>): T {
switch (r.kind) {
case"ok": return r.value; // r is the ok variant herecase"err": throw r.error;
}
}
Exhaustiveness checks with never — the compiler flags a missing case when the union grows:
functionassertNever(x: never): never {
thrownewError(`Unhandled variant: ${JSON.stringify(x)}`);
}
// in the switch: default: return assertNever(r); // TS2345 if a case is unhandled
Generics with constraints — accept the least you need; let call sites infer the rest:
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // return type is precise, not `unknown`
}
const name = pluck({ id: 1, name: "a" }, "name"); // typed as string
as const freezes literals to their narrowest readonly type; satisfies validates a value
against a type without widening it (you keep the precise inferred type):
Utility types — reach for the built-ins before writing mapped types by hand:
Partial<T>, Required<T>, Readonly<T>, Pick<T,K>, Omit<T,K>, Record<K,V>,
Exclude<U,X>, Extract<U,X>, NonNullable<T>, ReturnType<F>, Parameters<F>,
Awaited<T>, and NoInfer<T> (5.4, blocks inference from a given position).
Custom type guards & assertion functions encapsulate narrowing:
functionisString(x: unknown): x is string { returntypeof x === "string"; }
function assertDefined<T>(x: T): asserts x is NonNullable<T> {
if (x == null) thrownewError("expected defined");
}
Gotchas / best practices
any-creep. One any (a cast, an untyped dep, JSON.parse) silently infects everything it
touches. Prefer unknown at boundaries; strict (via noImplicitAny) catches implicit ones,
but explicit any and third-party any slip through — grep for them in review.
Structural typing surprises. Types match by shape, so an object with extra properties is
assignable to a narrower type — except the excess-property check fires only on fresh object
literals. Assigning through a variable bypasses it. Two unrelated types with the same shape are
interchangeable; use branded types when identity matters.
Object.keys / for..in return string[], not (keyof T)[] — deliberately, since objects
can hold extra keys at runtime. Cast or re-validate rather than assuming.
ESM/CJS interop. With verbatimModuleSyntax, type-only imports must use import type
(else TS2823-style emit errors). Under nodenext ESM, relative imports need explicit
extensions (./x.js, even from x.ts), and the package must declare "type": "module".
esModuleInterop makes import x from "cjs-pkg" work.
enum vs union. Prefer a union of string literals or an as const object over enum.
Numeric enums are unsafe (any number is assignable to them); const enum breaks under
isolatedModules/verbatimModuleSyntax. Literal unions are erasable, tree-shakeable, and
narrow cleanly.
Don't over-annotate. Let inference type locals and return values; annotate function
parameters, public API boundaries, and empty containers (const xs: string[] = []).
Reading common tsc errors:
Code
Meaning
Usual fix
TS2322
Type X not assignable to Y
shape/variance mismatch; check the diff TS prints
TS2345
Argument X not assignable to parameter Y
wrong arg type (also fired by assertNever)
TS2339
Property does not exist on type
narrow first, or type is wrong/too wide
TS18048 / TS2532
X is possibly 'undefined'
guard it; effect of strictNullChecks/noUncheckedIndexedAccess
TS18047
X is possibly 'null'
null guard
TS7006
Parameter implicitly has 'any'
add a parameter annotation
TS18046
X is of type 'unknown'
narrow before use
TS2769
No overload matches this call
args match no signature; read each listed overload
TS2367
Comparison appears unintentional
comparing non-overlapping types (often a bug)
Use this vs related skills
modern-python is the Python analogue (tooling + idioms); for React/Next specifics use the
frontend framework and design skills (e.g. vercel-react-best-practices) — this skill is the
language/type-system and tsconfig layer beneath them.