AgentSkillsCN

TypeScript

当用户提出“编写TypeScript”“创建TS模块”“实现TypeScript类”“建模这种类型”“添加类型”“为这个函数打上类型注解”“使用泛型”“使用品牌化类型”“使用区分联合类型”“修复这段TypeScript代码”“重构这段TS代码”“使用现代TypeScript”“配置tsconfig”或任何涉及TypeScript代码编写、审查或优化的任务时,应使用此技能。它提供了类型建模、现代语言特性(TypeScript 5.5–5.8)、错误处理、模块模式以及项目配置方面的最佳实践。此技能适用于TypeScript(.ts/.tsx)文件,而非仅限于带有JSDoc的纯JavaScript。

SKILL.md
--- frontmatter
name: TypeScript
version: 0.1.0
description: >-
  This skill should be used when the user asks to "write TypeScript",
  "create a TS module", "implement a TypeScript class", "model this type",
  "add types", "type this function", "use generics", "use branded types",
  "use discriminated unions", "fix this TypeScript", "refactor this TS code",
  "use modern TypeScript", "configure tsconfig", or any task involving
  writing, reviewing, or improving TypeScript code. Provides best practices
  for type modeling, modern language features (TypeScript 5.5-5.8),
  error handling, module patterns, and project configuration. This skill
  covers TypeScript (.ts/.tsx) files, NOT plain JavaScript with JSDoc.

TypeScript

Best practices for writing modern, type-safe TypeScript. Covers type modeling, modern compiler features (5.5-5.8), error handling, module patterns, and project configuration.

This skill targets .ts and .tsx files. For plain JavaScript with JSDoc type annotations, use the JavaScript skill instead.

Project Setup

tsconfig.json

Enable strict mode in tsconfig.json for every TypeScript project:

json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2024",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "noUncheckedIndexedAccess": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

Adjust target, module, and moduleResolution to match the project's runtime:

  • Node.js projects: "module": "nodenext", "moduleResolution": "nodenext"
  • Bundler-driven projects: "module": "preserve", "moduleResolution": "bundler"
  • Direct execution (Node.js 22.6+): Add "erasableSyntaxOnly": true to ensure all TS-specific syntax can be stripped without altering runtime behavior. Avoid enum, namespace, and parameter properties (constructor(public x: number)) when this flag is enabled.

Notable Compiler Options (5.5-5.8)

OptionVersionPurpose
noUncheckedSideEffectImports5.6Errors on unresolvable import "polyfill"
erasableSyntaxOnly5.8Restricts to type-erasable syntax for direct execution
verbatimModuleSyntax5.4Enforces explicit import type for type-only imports
noUncheckedIndexedAccess4.1Adds undefined to index signature results

Type Modeling

Prefer Inference Over Annotation

Let TypeScript infer types when the result is unambiguous. Annotate return types on public API surfaces and when inference would be too wide:

typescript
// Inferred — no annotation needed
const count = items.length;
const doubled = numbers.map(n => n * 2);

// Annotate: public API, inference would be too wide
function parseConfig(raw: string): AppConfig {
  return JSON.parse(raw) as AppConfig;
}

Discriminated Unions

Model variant types using a shared literal discriminant property. Use exhaustiveness checking with never to catch unhandled cases:

typescript
type Result<T> =
  | { kind: 'success'; value: T }
  | { kind: 'error'; error: Error };

function handle<T>(result: Result<T>): T {
  switch (result.kind) {
    case 'success': return result.value;
    case 'error': throw result.error;
    default: {
      const _exhaustive: never = result;
      throw new Error(`Unhandled case: ${_exhaustive}`);
    }
  }
}

The satisfies Operator

Use satisfies to validate a value conforms to a type while preserving narrower inference. Prefer satisfies over as for type validation:

typescript
type Route = { path: string; auth: boolean };
type Routes = Record<string, Route>;

// Type annotation: loses key knowledge
const annotated: Routes = { home: { path: '/', auth: false } };
annotated.home; // Route — no key autocomplete

// satisfies: validates AND preserves literal keys
const checked = {
  home: { path: '/', auth: false },
  dashboard: { path: '/dash', auth: true },
} satisfies Routes;
checked.home; // { path: string; auth: boolean } — keys autocomplete

// as const satisfies: preserves literal values too
const frozen = {
  home: { path: '/', auth: false },
} as const satisfies Routes;
frozen.home.path; // '/' (literal type)

Branded Types

Create nominally distinct types from the same underlying type to prevent accidental interchange:

typescript
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function createUserId(id: string): UserId {
  if (!id) throw new Error('Invalid user ID');
  return id as UserId;
}

function lookupUser(id: UserId): User { /* ... */ }
lookupUser(createUserId('abc'));  // OK
lookupUser('abc');                // Error: string is not UserId

NoInfer<T>

Use NoInfer (TS 5.4+) to control which parameter TypeScript infers a generic from:

typescript
function createFSM<TState extends string>(
  states: TState[],
  initial: NoInfer<TState>  // Must be one of the states, inferred from `states`
) { /* ... */ }

createFSM(['idle', 'running', 'done'], 'idle');     // OK
createFSM(['idle', 'running', 'done'], 'invalid');  // Error

Const Type Parameters

Use const type parameters (TS 5.0+) to infer literal types instead of widened types:

typescript
function createRoutes<const T extends readonly Route[]>(routes: T) {
  return routes;
}
// Infers tuple of literal types, not Route[]

For advanced patterns including template literal types, mapped types, conditional types, and complex generics, consult references/type-patterns.md.

Modern Language Features

Inferred Type Predicates (TS 5.5)

TypeScript 5.5 infers type predicates from function bodies. Manual type guards are no longer needed for simple narrowing:

typescript
// TS 5.5+: automatic inference, no annotation needed
const validUsers = users.filter(user => user !== null);
// Inferred as User[] (not (User | null)[])

Inference requires: a single return statement, no parameter mutation, and a boolean expression tied to a parameter refinement. Truthiness checks (!!x) infer predicates for object types but not for numbers (where 0 is falsy).

Explicit Resource Management (Stage 3)

Use using and await using for automatic resource cleanup:

typescript
function readFile(path: string) {
  using handle = openFile(path); // Symbol.dispose called at scope exit
  return handle.read();
}

async function connectDB() {
  await using conn = await pool.getConnection(); // Symbol.asyncDispose called
  return conn.query('SELECT 1');
}

Requires "lib": ["esnext.disposable"] in tsconfig. Check target runtime support — Chrome 134+, not yet cross-browser.

Error Handling

Error Cause Chains

Wrap errors with context using the cause option:

typescript
try {
  const data = JSON.parse(raw);
} catch (err) {
  throw new Error('Failed to parse config', { cause: err });
}

Result Pattern

Prefer discriminated union results over thrown exceptions for expected failure cases:

typescript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
  return { ok: true, value: await res.json() };
}

Reserve throw for truly exceptional / unrecoverable situations. Use Result for operations with expected failure modes (network requests, parsing, validation).

Module Patterns

ESM Conventions

  • Use "type": "module" in package.json
  • Prefer named exports over default exports for discoverability and refactoring
  • Use the "exports" field to define the package's public API
  • Avoid barrel files (index.ts re-exporting everything) in application code — they degrade tree-shaking, slow builds, and risk circular dependencies
  • Prefer direct imports: import { thing } from './utils/thing.js'
  • Use import type (or verbatimModuleSyntax) for type-only imports

Class Patterns

Use private fields (#field) and accessor keywords:

typescript
class Elevator {
  #currentFloor: number;
  #destination: number | null = null;

  constructor(startFloor: number) {
    this.#currentFloor = startFloor;
  }

  get currentFloor(): number {
    return this.#currentFloor;
  }
}

TypeScript 7 / Compiler Rewrite

The TypeScript compiler is being rewritten in Go (Project Corsa). TypeScript 7 is expected in 2026 with ~10x faster builds. Key migration considerations:

  • strict enabled by default
  • Minimum target es2015 (no more es5)
  • node10 module resolution removed — use nodenext or bundler
  • Standard LSP protocol replaces custom TSServer protocol
  • enum, namespace, and parameter properties remain supported but cannot be type-erased (relevant for erasableSyntaxOnly)

Avoid patterns that may complicate migration: deeply nested namespaces, complex enum merging, and reliance on custom TSServer APIs.

Additional Resources

  • references/type-patterns.md — Advanced type patterns: template literal types, mapped types, conditional types, complex generics, utility types, and type narrowing techniques.
  • references/api-reference.md — Quick reference for ES2024/2025 features, Web APIs, and TypeScript compiler options with usage examples and runtime support status.