AgentSkillsCN

js-ts-patterns

在创建 JS/TS 项目、选择框架,或构建高级类型模式时使用。涵盖工具链见解、类型设计,以及那些看似不起眼却容易踩坑的细节。

SKILL.md
--- frontmatter
name: js-ts-patterns
description: Use when creating JS/TS projects, choosing frameworks, or building advanced type patterns. Covers tooling opinions, type decisions, and non-obvious gotchas.

JS/TS Patterns

Tooling opinions, type-level decisions, and non-obvious patterns. Opus knows the basics -- this covers what to choose and when.

Style Guide

Source: Google JS + TS Style Guides. Only rules linters/formatters cannot enforce.

Naming

  • Treat abbreviations as whole words: loadHttpUrl not loadHTTPURL
  • Single-letter names only within ≤10-line scope
  • Never abbreviate by deleting letters: customerId not cstmrId
  • Descriptive over terse: errorCount not errCnt
  • Boolean names: isVisible, hasPermission, canEdit
  • Event handlers: onEventName pattern (onClick, onSubmit)
  • Test methods: testX_whenY_doesZ for structured names
  • CONSTANT_CASE only for deeply immutable module-level values, not local const

Practices

  • Named exports only — no default exports
  • Prefer interface over type for object shapes
  • Avoid type assertions (as); use runtime checks
  • Catch errors as unknown, assert Error type
  • == null is the one acceptable == (catches null + undefined)
  • Function declarations at module level, arrow functions for callbacks
  • No getters/setters unless framework-required
  • Modules for namespacing, not static-only container classes

Tooling Defaults

ConcernUseWhy
Package managerpnpmFaster, disk-efficient, strict by default
BundlerVite (apps), tsup (libs)Fast, sensible defaults
Test runnerVitestVite-native, Jest-compatible API
LinterESLint + @typescript-eslintCatches real bugs
FormatterBiome or PrettierOpinionated, zero config
Runtime (scripts)tsxTS execution without build step

Project Types

TypeWhenInit
Next.jsFull-stack React, SSR/SSGpnpm create next-app@latest
React + ViteSPA, component libspnpm create vite . --template react-ts
Node.js APIExpress/Fastify backendsManual setup with tsx
LibraryNPM packagestsup for bundling
CLICommand-line toolscommander or yargs

TypeScript Config Opinions

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "paths": { "@/*": ["./src/*"] }
  }
}
  • Always strict: true -- no exceptions
  • moduleResolution: "bundler" for apps (Vite/Next), "node16" for libraries
  • skipLibCheck: true -- speeds up compilation, rarely hides real bugs
  • "type": "module" in package.json for all new projects

Advanced Type Patterns

When to Reach for Advanced Types

NeedPattern
Type-safe eventsRecord<EventName, Payload> + generic on/emit
Typed API clientMapped type over endpoint config
State machinesDiscriminated unions + exhaustive switch
Deep immutabilityRecursive DeepReadonly<T>
Config pathsTemplate literal recursion: "server.host"

Discriminated Unions (most underused pattern)

typescript
type Result<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }
  | { status: 'loading' };

// Compiler enforces all cases
function handle<T>(r: Result<T>) {
  switch (r.status) {
    case 'success': return r.data;
    case 'error': throw new Error(r.error);
    case 'loading': return null;
  }
}

Key Remapping

typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
};

infer Patterns

typescript
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;

Branded Types (nominal typing)

typescript
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };

function getUser(id: UserId) { /* ... */ }
// getUser("abc")          -- compile error
// getUser("abc" as UserId) -- ok

Type Testing

typescript
type AssertEqual<T, U> = [T] extends [U] ? [U] extends [T] ? true : false : false;
type _test1 = AssertEqual<string, string>;  // true
type _test2 = AssertEqual<string, number>;  // false

Runtime Patterns

Library package.json

json
{
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "prepublishOnly": "pnpm build"
  }
}

Type Guards (prefer over assertions)

typescript
function isString(v: unknown): v is string { return typeof v === 'string'; }

// Assertion function (narrows in-place)
function assertDefined<T>(v: T | undefined): asserts v is T {
  if (v === undefined) throw new Error('undefined');
}

Framework Decisions

Express vs Fastify

ExpressFastify
SpeedBaseline~2x faster
TypeScriptBolted-on typesFirst-class
Schema validationManual/middlewareBuilt-in (Ajv)
EcosystemMassiveGrowing
Choose whenTeam knows itNew projects

Zod vs Joi vs class-validator

ZodJoiclass-validator
TS inferenceNativeNoneNone
StyleFunctionalChainingDecorators
Choose whenDefault choiceLegacy JSNestJS

type vs interface

Use interfaceUse type
Object shapesUnions, intersections
Extendable contractsMapped/conditional types
Better error messagesComplex type algebra

Rule: start with interface, switch to type when you need union/conditional.

Gotchas

  • any defeats the type system -- use unknown and narrow
  • Avoid enum -- use as const objects or union types (better tree-shaking)
  • satisfies preserves literal types while checking: const x = { a: 1 } satisfies Record<string, number>
  • interface extends > type & -- intersections can produce never silently
  • Deeply nested conditional types slow tsc -- flatten when possible
  • Circular type references crash tsc -- break cycles with intermediate types
  • as const to preserve literal types in arrays/objects