Skip to content

Latest commit

 

History

History
425 lines (291 loc) · 15.4 KB

File metadata and controls

425 lines (291 loc) · 15.4 KB

TYPESCRIPT.md

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)


0. Global Rules For This Agent

  • 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 any unless absolutely necessary; prefer unknown, then narrow.
  • Respect existing tsconfig.json if 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.

1. Project Setup & tsconfig (TS 5.9+)

1.1 Baseline tsconfig Defaults (5.9 tsc --init)

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 using module: "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 yield T | 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: true
    • declaration: true
    • declarationMap: true

When editing tsconfig:

  • Don’t silently loosen strict flags; if you must, document why.
  • Avoid changing moduleResolution away from:
    • "node20" or "nodenext" for Node.
    • "bundler" for bundler-based apps (Vite, Webpack, etc).
  • Prefer lib: ["esnext", "DOM"] in browser apps; ["esnext"] plus @types/node in 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.

2. Modules, Imports & Interop

2.1 ES Modules

  • Use import / export syntax 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.

2.2 import defer (TS 5.9+)

  • 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.

2.3 Node Module Modes

  • 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 require and import unless you are explicitly in a CommonJS file and interop is required.

3. Core Type System Concepts To Apply

3.1 Important Built-in Types

  • unknown: safer alternative to any. Always narrow before using.
  • any: last resort; avoid introducing new any usage.
  • 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 const to freeze object/array literals and get literal types.

3.2 Unions & Intersections

  • 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).

3.3 Interfaces vs Type Aliases

  • Use interfaces when:
    • You expect extension via extends or declaration merging.
    • Representing object-shaped contracts for public APIs.
  • 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.

3.4 Generics

  • 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.

3.5 Template Literal Types

  • 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.

3.6 Conditional Types & infer

  • Conditional types: T extends U ? X : Y

  • Use infer for 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.

3.7 Utility Types To Prefer

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.

4. Narrowing & Control Flow

4.1 Standard Narrowing Techniques

Use control-flow based narrowing aggressively:

  • typeof for primitives: typeof value === "string"
  • instanceof for class instances.
  • in operator for property-based narrowing: "kind" in x
  • Equality checks: if (x === null), if (x === undefined)
  • Truthiness checks: with care (avoid depending on 0, "", false truthiness for business logic).

4.2 Discriminated Unions

Pattern:

  • Every variant has a literal kind:

    • { kind: "success"; data: Data } | { kind: "error"; error: Error }
  • Switch on kind and ensure exhaustiveness:

    • Add a default/never branch that throws if a new kind appears and is not handled.

4.3 Custom Type Predicates

  • Use functions returning value is Type to 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.


5. Functions & Async Code

5.1 Function Types

  • 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 Function or (...args: any[]) => any except at generic utility boundaries.

5.2 Overloads vs Unions

  • 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 separate fnString, fnNumber.

5.3 Async / Await

  • All async functions must return Promise<T> with a concrete T.
  • Use Awaited<T> when manipulating async results at the type level.
  • Use top-level await only when module target/runtime supports it.

6. Classes, Fields & Objects

6.1 Classes

  • Prefer classes only when you need:
    • State + methods packaged together.
    • Inheritance (limited, shallow).
    • Integration with frameworks expecting classes.
  • Use implements to connect classes to interface contracts.
  • Prefer composition over inheritance.

6.2 Fields & Modifiers

  • public, private, protected, readonly and override should be used explicitly in public APIs.

  • Prefer parameter properties to reduce boilerplate in constructors:

    • constructor(private readonly repo: Repo) {}
  • Use readonly for values not intended to change after construction.

6.3 satisfies Operator

  • Use satisfies to validate shapes without widening inferred types:

    • const routes = { home: "/", about: "/about" } as const satisfies Record<string, string>
  • This keeps routes.home typed as "/" instead of string.


7. Modern & Advanced Features (TS 5.x Era)

This section focuses on “recent-ish” features that an older model might miss. Use them where appropriate.

7.1 TypeScript 5.9 Highlights

  • Minimal tsc --init:
    • Generates a concise tsconfig.json with:
      • modern module and target defaults
      • strict and recommended diagnostics
      • JSX default (react-jsx)
      • verbatimModuleSyntax, isolatedModules, moduleDetection: "force".
  • 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.
  • 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 .buffer where 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.

7.2 TypeScript 5.8 Highlights

  • Granular checks for return expressions:
    • Conditional expressions directly in return are checked branch-by-branch against the return type.
    • This catches any-driven mistakes where one branch type is wrong.
  • --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).

7.3 TypeScript 5.7 Highlights

  • 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.
  • --target es2024 and --lib es2024:
    • Support for ES2024, including Object.groupBy, Map.groupBy, Promise.withResolvers, and updated buffer types.

7.4 TypeScript 5.6 Highlights

  • Disallowed always-truthy/nullish checks:
    • Errors on obviously incorrect conditions like:
      • if (/regex/) { ... }
      • if (x => 0) { ... }
      • Mis-parenthesised ?? usages.
  • Region-prioritized diagnostics:
    • Editor-only improvement; errors surface faster in the visible region.
  • --stopOnBuildErrors in --build mode:
    • Stops multi-project builds when a project has errors.

7.5 TypeScript 5.5 Highlights

  • 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.

7.6 Older but Important (Still Relevant)

The agent should also know these (common in modern code):

  • Standard decorators (5.0):
    • Stage 3 decorators supported; avoid legacy experimentalDecorators where possible.
  • 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):
    • using declarations with [Symbol.dispose] / [Symbol.asyncDispose] for structured cleanup.
    • Use for advanced resource management in environments that support the feature.

8. Resource Management: using (If Enabled)

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.
  • This compiles down to try/finally in JS where supported.

Avoid introducing this in projects that don’t already opt into the proposal.


9. Interop, .d.ts and JS Projects

9.1 JS Interop

  • For JS codebases with types:
    • Use // @ts-check and JSDoc annotations.
    • Generate .d.ts via tsc --declaration --emitDeclarationOnly.
  • For consuming JS libraries:
    • Prefer @types/<lib> if available.
    • For libraries without types, write minimal .d.ts that covers the used surface.

9.2 Writing .d.ts Files

  • Represent public surface only.
  • Use declare for ambient declarations:
    • declare module "pkg" { ... }
    • declare global { interface Window { ... } }
  • Avoid implementation details, unions that are too broad, and unnecessary any.

10. Patterns & Anti-Patterns for the Agent

10.1 Do

  • Use modern config defaults described in section 1.
  • Use unions + discriminants instead of long chains of optional fields.
  • Use unknown for external data and narrow early.
  • Use satisfies to keep inferred literals while satisfying wider contracts.
  • Add explicit types at public boundaries (exports, API handlers, event handlers).

10.2 Don’t

  • Don’t introduce new any or ! (non-null assertions) casually.
  • Don’t change strict from true to false unless a human explicitly wants it.
  • Don’t rely on deprecated patterns:
    • namespaces, triple-slash references, export =/import = (unless project already does).
  • Don’t emit code that assumes decorators are the old experimental version when standard decorators are configured.

11. References (For the Agent, Not For Output)

When more detail is needed, the agent may consult: