Goal: give an AI agent enough context to write modern TypeScript 5.9+ code, with correct project setup and idiomatic patterns. Assume good knowledge of JavaScript.
For full language docs, see: (link here: https://www.typescriptlang.org/docs/handbook/intro.html)
- Always target TypeScript 5.9+ semantics.
- Prefer ES modules, not namespaces or legacy
/// <reference />unless a project already uses them. - Prefer strict, explicit types, even if inference would work.
- Avoid
anyunless absolutely necessary; preferunknown, then narrow. - Respect existing
tsconfig.jsonif present. If creating one, use the modern defaults below. - Prefer composition and data-first types (unions, mapped types, utility types) over heavy class hierarchies.
- Keep declaration files (
.d.ts) clean and minimal if you have to author them.
When creating a new project, assume a config roughly equivalent to:
module:"nodenext"or"node20"for Node runtimes.target:"esnext"(for latest features) or"es2023"when usingmodule: "node20".jsx:"react-jsx"in React projects.strict:true.verbatimModuleSyntax:true(don’t downlevel ESM syntax).isolatedModules:true(safe for single-file transforms like Babel / esbuild / SWC).moduleDetection:"force"(treat every file as a module).noUncheckedIndexedAccess:true(index signatures yieldT | undefined).exactOptionalPropertyTypes:true(optional props behave precisely).noUncheckedSideEffectImports:true(flag imports that are only side-effects).skipLibCheck:true(faster builds, OK for most projects).types:[]by default, then add specific@types/*as needed.- Generate maps and declarations by default for libraries:
sourceMap: truedeclaration: truedeclarationMap: true
When editing tsconfig:
- Don’t silently loosen
strictflags; if you must, document why. - Avoid changing
moduleResolutionaway from:"node20"or"nodenext"for Node."bundler"for bundler-based apps (Vite, Webpack, etc).
- Prefer
lib: ["esnext", "DOM"]in browser apps;["esnext"]plus@types/nodein Node apps.
For a fresh Node 20 project, a safe minimal base is:
module:"node20"moduleResolution:"node20"target:"es2023"lib:["es2023"]- plus the strict/diagnostic flags above.
-
Use
import/exportsyntax everywhere. -
Prefer named exports over default exports for library code.
-
Use type-only imports/exports to avoid runtime dependencies:
import type { Foo } from "./foo.js"export type { Foo } from "./foo.js"
-
For dynamic loading, use
await import("./module.js")in async contexts.
-
Use for deferred evaluation of modules that have heavy side effects.
-
Syntax is namespace-only:
import defer * as feature from "./some-feature.js"
-
Not allowed:
import defer { something } from "pkg"import defer defaultExport from "pkg"
-
Semantics:
- Module is loaded immediately but executed only on first property access.
- Works only with
module: "esnext"or"preserve"and runtimes/bundlers that support the proposal.
-
Use when:
- Expensive initialisation is rarely needed.
- Feature flags or environment-specific modules should not run at startup.
- Use
module: "node20"/moduleResolution: "node20"to model modern Node 20 behaviour. - Use
"nodenext"only if you need evolving semantics or compatibility with older TS configs. - Avoid mixing
requireandimportunless you are explicitly in a CommonJS file and interop is required.
unknown: safer alternative toany. Always narrow before using.any: last resort; avoid introducing newanyusage.never: impossible value; use to detect unreachable code and exhaustive switches.void: functions that don’t return a useful value.- Literal types:
const value = "ok"gives type"ok"(string literal).- Use
as constto freeze object/array literals and get literal types.
-
Prefer unions of object types for variant shapes:
type Result = { ok: true; data: Data } | { ok: false; error: Error }
-
Use a discriminant (tag) property (e.g.
kind,type,status) to make narrowing trivial. -
Use intersections primarily for:
- Combining cross-cutting concerns, e.g.
User & WithTimestamps. - Extending types (but prefer interfaces or mapped types when expressive).
- Combining cross-cutting concerns, e.g.
- Use interfaces when:
- You expect extension via
extendsor declaration merging. - Representing object-shaped contracts for public APIs.
- You expect extension via
- Use type aliases when:
- You need unions, intersections, mapped types, conditional types, or tuples.
- For new code, either is fine for plain object shapes; choose the one that fits future constraints.
-
Use generics to parameterise over types:
function wrap<T>(value: T): { value: T } { ... }
-
Constrain generics with
extends:function getKey<T extends object, K extends keyof T>(obj: T, key: K): T[K]
-
Use default type parameters to keep call-sites simple:
type ApiResponse<T = unknown> = { ok: boolean; data: T }
-
Avoid over-generic functions when a union is clearer.
-
Use template literal types to encode string patterns:
-
type EventName =user:${"created" | "deleted"}``
-
-
Use with
keyof/mapped types to transform keys:-
type PrefixKeys<T, P extends string> = { [K in keyof T as${P}${Extract<K, string>}]: T[K] }
-
Use these instead of brittle string-constants when encoding naming conventions.
-
Conditional types:
T extends U ? X : Y -
Use
inferfor derived types:type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
-
Common patterns:
- Extracting element type from an array.
- Extracting return type from a function.
- Transforming union constituents one-by-one.
Use built-in utilities instead of reinventing them:
Partial<T>,Required<T>,Readonly<T>Pick<T, K>,Omit<T, K>Record<K, T>ReturnType<F>,Parameters<F>InstanceType<C>NonNullable<T>Awaited<T>for async results- Use them as building blocks in your own mapped types.
Use control-flow based narrowing aggressively:
typeoffor primitives:typeof value === "string"instanceoffor class instances.inoperator for property-based narrowing:"kind" in x- Equality checks:
if (x === null),if (x === undefined) - Truthiness checks: with care (avoid depending on
0,"",falsetruthiness for business logic).
Pattern:
-
Every variant has a literal
kind:{ kind: "success"; data: Data } | { kind: "error"; error: Error }
-
Switch on
kindand ensure exhaustiveness:- Add a
default/neverbranch that throws if a newkindappears and is not handled.
- Add a
-
Use functions returning
value is Typeto encapsulate checks:function isUser(value: unknown): value is User { ... }
-
These enable re-use of complex narrowing logic, keep call-sites clean, and help the checker.
- Prefer arrow functions for inline and callbacks, named functions for top-level APIs.
- Use parameter and return types explicitly for public APIs; rely on inference for locals if obvious.
- Avoid
Functionor(...args: any[]) => anyexcept at generic utility boundaries.
-
Use overloads when the return type depends on parameter combinations and can’t be expressed with a simple generic or union.
-
Otherwise, use unions or generics:
- Prefer
fn(value: string | number)over separatefnString,fnNumber.
- Prefer
- All
asyncfunctions must returnPromise<T>with a concreteT. - Use
Awaited<T>when manipulating async results at the type level. - Use top-level
awaitonly when module target/runtime supports it.
- Prefer classes only when you need:
- State + methods packaged together.
- Inheritance (limited, shallow).
- Integration with frameworks expecting classes.
- Use
implementsto connect classes to interface contracts. - Prefer composition over inheritance.
-
public,private,protected,readonlyandoverrideshould be used explicitly in public APIs. -
Prefer parameter properties to reduce boilerplate in constructors:
constructor(private readonly repo: Repo) {}
-
Use
readonlyfor values not intended to change after construction.
-
Use
satisfiesto validate shapes without widening inferred types:const routes = { home: "/", about: "/about" } as const satisfies Record<string, string>
-
This keeps
routes.hometyped as"/"instead ofstring.
This section focuses on “recent-ish” features that an older model might miss. Use them where appropriate.
- Minimal
tsc --init:- Generates a concise
tsconfig.jsonwith:- modern module and target defaults
- strict and recommended diagnostics
- JSX default (
react-jsx) verbatimModuleSyntax,isolatedModules,moduleDetection: "force".
- Generates a concise
import defer:- Namespace-only deferred module evaluation.
- Use for lazy evaluation with heavy side effects.
- Only valid for
module: "esnext" | "preserve"and compatible runtimes/bundlers.
--module node20/--moduleResolution node20:- Stable Node.js v20 behaviour; less likely to change than
"nodenext". - Implies
target: "es2023"by default.
- Stable Node.js v20 behaviour; less likely to change than
- DOM & lib changes:
- More precise DOM type summaries and stricter buffer/typed array relationships.
- When errors appear around
ArrayBuffer/Uint8Array/Buffer, prefer explicit types and use.bufferwhere needed.
- Type argument inference tweaks:
- In some complex generics, inference is stricter to avoid leaks.
- If you see new inference errors, fix by adding explicit type arguments or narrowing types.
- Granular checks for return expressions:
- Conditional expressions directly in
returnare checked branch-by-branch against the return type. - This catches
any-driven mistakes where one branch type is wrong.
- Conditional expressions directly in
--module node18:- Stable flag for Node 18 behaviour; useful if you must stay on Node 18.
--erasableSyntaxOnly:- Ensures emit only erases types and leaves runtime JS semantics unchanged.
- Use when you want to guarantee TS is not rewriting logic.
--libReplacement:- Allows library authors to replace built-in libs with versions from
node_modules(advanced; avoid toggling unless necessary).
- Allows library authors to replace built-in libs with versions from
- Stronger checks for never-initialized variables:
- Catches variables that are never assigned before use, even across nested functions.
- Path rewriting for relative paths with “in-place” runtimes:
- Helps when using Node’s
--experimental-strip-types, ts-node, tsx, Deno, Bun, etc.
- Helps when using Node’s
--target es2024and--lib es2024:- Support for ES2024, including
Object.groupBy,Map.groupBy,Promise.withResolvers, and updated buffer types.
- Support for ES2024, including
- Disallowed always-truthy/nullish checks:
- Errors on obviously incorrect conditions like:
if (/regex/) { ... }if (x => 0) { ... }- Mis-parenthesised
??usages.
- Errors on obviously incorrect conditions like:
- Region-prioritized diagnostics:
- Editor-only improvement; errors surface faster in the visible region.
--stopOnBuildErrorsin--buildmode:- Stops multi-project builds when a project has errors.
- Improved type inference:
- Inferred type predicates in more cases.
- Better control-flow narrowing for constant indexed accesses (e.g.
obj[key]narrowing based on checks).
- Regex syntax checking:
- Basic validation of regular expression literals.
- New Set helpers:
- Type support for new ECMAScript Set methods.
The agent should also know these (common in modern code):
- Standard decorators (5.0):
- Stage 3 decorators supported; avoid legacy
experimentalDecoratorswhere possible.
- Stage 3 decorators supported; avoid legacy
- Const type parameters (5.0):
function makeMap<const K extends string>(keys: K[]): Record<K, number> { ... }- Keeps literal information in generics.
- Explicit resource management (5.2):
usingdeclarations with[Symbol.dispose]/[Symbol.asyncDispose]for structured cleanup.- Use for advanced resource management in environments that support the feature.
Only use if the project targets runtimes supporting explicit resource management proposal.
-
Pattern:
- Provide
[Symbol.dispose]()or[Symbol.asyncDispose]()on classes/resources. - Use
using resource = createResource()to ensure disposal at the end of the scope.
- Provide
-
This compiles down to try/finally in JS where supported.
Avoid introducing this in projects that don’t already opt into the proposal.
- For JS codebases with types:
- Use
// @ts-checkand JSDoc annotations. - Generate
.d.tsviatsc --declaration --emitDeclarationOnly.
- Use
- For consuming JS libraries:
- Prefer
@types/<lib>if available. - For libraries without types, write minimal
.d.tsthat covers the used surface.
- Prefer
- Represent public surface only.
- Use
declarefor ambient declarations:declare module "pkg" { ... }declare global { interface Window { ... } }
- Avoid implementation details, unions that are too broad, and unnecessary
any.
- Use modern config defaults described in section 1.
- Use unions + discriminants instead of long chains of optional fields.
- Use
unknownfor external data and narrow early. - Use
satisfiesto keep inferred literals while satisfying wider contracts. - Add explicit types at public boundaries (exports, API handlers, event handlers).
- Don’t introduce new
anyor!(non-null assertions) casually. - Don’t change
strictfromtruetofalseunless a human explicitly wants it. - Don’t rely on deprecated patterns:
- namespaces, triple-slash references,
export =/import =(unless project already does).
- namespaces, triple-slash references,
- Don’t emit code that assumes decorators are the old experimental version when standard decorators are configured.
When more detail is needed, the agent may consult:
- TypeScript Handbook entry point: (link here: https://www.typescriptlang.org/docs/handbook/intro.html)
- Release notes (5.9 → 5.5): (link here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-9.html)
- TSConfig reference: (link here: https://www.typescriptlang.org/tsconfig)
- Module docs & ESM/CJS interop: (link here: https://www.typescriptlang.org/docs/handbook/modules.html)