Writing Skills
writing-skills
效率与协作技能,来自 obra/superpowers,查看原文获取完整用途和操作说明。
typescript【部署与运维】Use this skill when the user asks about TypeScript build tools, tsconfig, module resolution, type system, advanced types, generics, testing, or production deployment. Focus on tooling, type system, and ecosystem — not syntax basics.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Guide for TypeScript build tools, tsconfig configuration, module resolution, advanced type system (generics, conditional types, template literals), testing, and production deployment.
Keywords: typescript build, tsconfig, module resolution, tsc, esbuild, generics typescript, conditional types, type narrowing, vitest, jest typescript.
Target runtime?
├── Node.js 22+ (modern) → ESM only ("type": "module" in package.json)
├── Node.js 18-20 → ESM + CJS fallback (dual package)
├── Library for wide consumption → ESM + CJS with "exports" field
├── Browser app → Bundled ESM via esbuild/webpack
└── Mixed-version Node.js + browsers → tsc + bundler with moduleResolution "bundler"
What are you building?
├── Library → tsup (esbuild-based, ESM+CJS dual output, DTS generation)
├── Node.js app → tsx (dev) + tsc (build) OR Bun for all-in-one
├── Browser app → Vite (esbuild dev, rollup prod) with TypeScript plugin
├── Monorepo → Turborepo + tsup per package + tsc --noEmit for type-check
└── Quick script → tsx (execute TS directly, no build step)
Codebase maturity?
├── New project → strict: true + noUncheckedIndexedAccess + exactOptionalPropertyTypes
├── Migrating JS → strict: true first, incremental strictness per file
├── Library → strict + declaration + declarationMap
├── Monorepo → project references + composite: true + incremental
└── Maximum safety → strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes + noPropertyAccessFromIndexSignature
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true // For type-check only; use tsup/vite for emit
},
"include": ["src/**/*"]
}
{
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./client": {
"import": "./dist/client.js",
"require": "./dist/client.cjs",
"types": "./dist/client.d.ts"
}
}
}
# Type-check (no emit)
tsc --noEmit
# Library build
tsup src/index.ts --format esm,cjs --dts --clean
# App build
tsc -p tsconfig.build.json
node --experimental-specifier-resolution=node dist/index.js
# Watch mode
tsup src/index.ts --format esm --watch
# API Extractor for .d.ts rollup
api-extractor run
// Prevent mixing semantically different strings/numbers
type Brand<T, B> = T & { __brand: B };
type OrderId = Brand<string, "OrderId">;
type UserId = Brand<string, "UserId">;
function createOrderId(value: string): OrderId {
return value as OrderId;
}
function getOrder(id: OrderId): Order { ... }
function getUser(id: UserId): User { ... }
// TypeScript catches the mistake:
const uid = createUserId("abc");
getOrder(uid); // ❌ Type 'UserId' not assignable to 'OrderId'
// Exhaustive matching with exhaustiveness check
type ApiResponse<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error; code: number };
function handleResponse<T>(resp: ApiResponse<T>): void {
switch (resp.status) {
case "loading":
break;
case "success":
console.log(resp.data);
break;
case "error":
console.error(resp.error, resp.code);
break;
default:
// Ensures all cases handled — compile error if new type added
const _exhaustive: never = resp;
}
}
// Path parameters as types
type EventName = `user:${"created" | "updated" | "deleted"}`;
// EventName = "user:created" | "user:updated" | "user:deleted"
// Parse path params
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type UserRouteParams = ExtractParams<"/users/:userId/orders/:orderId">;
// { userId: string; orderId: string }
// Extract return type from function
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
// Deep partial
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
// Non-nullable keys
type NonNullableKeys<T> = {
[K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
};
class QueryBuilder<T extends Record<string, unknown>> {
private filters: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters[key] = value;
return this;
}
build(): Partial<T> {
return { ...this.filters };
}
}
type Order = { status: "pending" | "paid"; total: number; customer: string };
const query = new QueryBuilder<Order>()
.where("status", "pending")
.where("total", 100)
.build();
import { describe, it, expect, vi, beforeEach } from "vitest";
import { processOrder } from "./order";
const mockDb = { save: vi.fn() };
beforeEach(() => {
vi.clearAllMocks();
});
describe("processOrder", () => {
it("creates order with pending status", async () => {
mockDb.save.mockResolvedValue({ id: 1 });
const result = await processOrder({ customerId: 1, items: [] }, mockDb);
expect(result.status).toBe("pending");
expect(mockDb.save).toHaveBeenCalledOnce();
});
it("throws if items empty", async () => {
await expect(
processOrder({ customerId: 1, items: [] }, mockDb)
).rejects.toThrow("order must have items");
});
});
import { expectTypeOf } from "vitest";
it("ExtractParams extracts route params", () => {
type Result = ExtractParams<"/users/:userId/orders/:orderId">;
expectTypeOf<Result>().toEqualTypeOf<{
userId: string;
orderId: string;
}>();
});
any everywhere: Defeats TypeScript's entire purpose. Use unknown + type narrowing, or @ts-expect-error with commentas casting without validation: Silences compiler but allows runtime errors. Use runtime validation (zod, io-ts) and infer types// @ts-ignore over // @ts-expect-error: ignore silences all errors including future ones. Use expect-error with next-line specificityenum when union is sufficient: enum values exist at runtime and tree-shake poorly. Use as const + union typeimport/exportsatisfies: as const loses type info. satisfies validates without wideningnull vs undefined inconsistency: Pick one convention. Prefer undefined for missing valuesnoUncheckedIndexedAccess disabled: Allows reading [i] without bounds, leading to undefined crashesskipLibCheck disabled in large projects: Slows typecheck dramatically. Keep enabled unless building librarytsc --noEmit for type-check only (separate from bundler for speed)skipLibCheck: true in app projects (library type resolution takes 60%+ of typecheck time)incremental: true + tsbuildinfo for faster incremental type-checksinterface over type for object shapes (faster, mergeable, better error messages)const assertions over as const for arrays: ["a", "b"] as const creates readonly tuplesatisfies keyword for type validation without widening (TypeScript 4.9+)isolatedModules: true ensures each file can be transpiled independently (required by esbuild)verbatimModuleSyntax prohibits type-only imports without type prefixTypeScript types are erased at runtime — external data (API responses, form inputs, localStorage) must be validated at runtime. Zod is the standard: define schemas that double as TypeScript types via z.infer<typeof schema>. Pattern:
import { z } from 'zod';
const OrderSchema = z.object({
id: z.string().uuid(),
customerId: z.number().positive(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
})).min(1),
status: z.enum(['pending', 'paid', 'shipped', 'cancelled']),
total: z.number().nonnegative(),
createdAt: z.string().datetime(),
});
type Order = z.infer<typeof OrderSchema>;
// Validate at boundary
function parseOrder(data: unknown): Order {
return OrderSchema.parse(data);
}
Key patterns: (a) parse at system boundaries (API, file read, storage), (b) infer types from schemas (don't duplicate), (c) use .safeParse() for error handling, (d) transform with .transform(), (e) compose schemas with .merge(), .pick(), .omit(). This eliminates the any from external data sources.
Monorepo with multiple TypeScript packages needs careful config:
build, test, lint per package. Remote caching with Vercel.pnpm-workspace.yaml.tsconfig.json with composite: true and references. Build with tsc --build for incremental compilation.@repo/typescript-config package with base tsconfigs extended by all packages.Package exports: each package defines "exports" map for public API, keeps internal code private. Use "types" export condition for DTS files. Scripts: turbo run build --filter=@repo/api for targeted builds.
Server Components by default, Client Components when you need interactivity. Data fetching: async function Page() directly fetches in Server Component — no useEffect, no useSWR, no loading states needed for initial data. Mutations: Server Actions ('use server') handle form submissions and data mutations without API routes. Caching: fetch() is cached by default, use cache: 'no-store' or next: { revalidate: 60 } for dynamic data. Loading/error: loading.tsx and error.tsx files at each route segment. Layouts: layout.tsx persists across navigations (doesn't remount). Route groups: (marketing) and (dashboard) groups have separate layouts.
Client Component optimization: (a) push state management down, (b) use useMemo/useCallback sparingly (Server Components make these often unnecessary), (c) bundle size — use next/dynamic for heavy client components. Forms: Server Actions with useActionState (React 19) for progressive enhancement.
| Feature | Vitest | Jest | Bun:test | Mocha |
|---|---|---|---|---|
| Speed | Fast (esbuild transform) | Medium | Fastest | Slow |
| TypeScript native | Yes | Via babel/ts-jest | Yes | Yes |
| Type testing | expectTypeOf | expect with jest-extended | expectTypeOf | Third-party |
| Browser testing | Playwright | jsdom/Playwright | Limited | jsdom |
| ESM support | Native | Experimental | Native | Good |
| Watch mode | Built-in, fast | Built-in | Yes | Chokidar |
| Snapshot | Built-in | Built-in | Built-in | Third-party |
Recommendation: Vitest for all TypeScript projects — fastest, native TS, compatible with Jest ecosystem plugins.
How are modules resolved?
├── Node.js app, modern → moduleResolution: "NodeNext"
│ Requires explicit .js/.mjs extensions in imports: `import "./utils.js"`
├── Bundler (esbuild, Vite, tsup) → moduleResolution: "bundler"
│ No extensions needed in imports, fastest resolution
│ Must set `allowImportingTsExtensions: true` for direct TS imports
├── Library for Node.js consumers → Dual: "NodeNext" for ESM, "node16" for CJS
│ Test both paths in CI
└── Deno / Bun → "NodeNext" + `--experimental-specifier-resolution=node` or native
External data entering the TypeScript system?
├── API response (JSON.parse / fetch) → Zod schema at fetch call site
│ const schema = z.object({...});
│ const data = schema.parse(response); // throws if invalid
├── localStorage / AsyncStorage → Zod parse on read, toJSON on write
│ Storage is unstructured — validate every read
├── URL query params (searchParams) → Zod schema per route
│ Query params are strings — coerce to correct type
├── Form data (user input) → Zod schema at submit handler
│ Validate on submit, display field-level errors
└── WebSocket / Server-Sent Events → Zod discrimated union
Parse each message type, dispatch to typed handlers
Type of application state?
├── Server data (API results, entity cache) → TanStack Query / RTK Query
│ Automatic cache invalidation, refetch, optimistic updates
│ Type-safe via Zod → QueryResult<Order[]>
├── Client state (UI state, form, modals) → Zustand / Jotai
│ Minimal boilerplate, TypeScript native
│ Zustand: single store with selectors, Jotai: atomic signals
├── URL state (route params, query strings) → React Router / Next.js navigation
│ Source of truth in URL — persist/resume state via navigation
├── Form state → React Hook Form + Zod resolver
│ Type-safe form validation, dirty tracking, error mapping
└── Global shared state → Context API (small apps) or Zustand
Context triggers re-render on all consumers. Zustand only re-renders on selected slices.
as casting API responses: response.data as Order assumes valid data at compile time — runtime may differ. Use Zod .parse().// @ts-expect-error without comment: Future maintainers won't know why the error is suppressed. Always add a reason.namespace: Legacy TypeScript pre-ESM feature. Use ES modules (import/export) for all code organization.default and named exports: Causes import confusion. Stick to named exports exclusively for better tree-shaking.index.ts): Re-exporting everything creates circular import risks and slows down type checking.enum with computed values: Loses type safety. Define with string literals: type Status = "active" | "inactive".typeof checks on runtime values: TypeScript types are erased. Use Zod .safeParse() or user-defined type guards.strictNullChecks: The single most impactful setting. Without it, null and undefined pass through silently.as any escape hatch: Propagates silently through your type system. Use satisfies or @ts-expect-error with intent.declarationMap: Consumers of your library can't "Go to Definition" into source. Always include .d.ts.map.tsup for tree-shaking — tsup --treeshake removes unused exports. Analyze with bundle-buddy or analyze.tsc --noEmit exclusively for type checking (not for compilation). --incremental caches .tsbuildinfo.skipLibCheck: true: Library type resolution takes 60%+ of total type checking time. Keep enabled for app projects.isolatedModules: true: Required for esbuild/Vite transpilation. Ensures each file is independently transformable.verbatimModuleSyntax (TS 5.0+): Prevents type-only imports without type prefix — ensures they're elided at runtime.noEmit: true + tsc --noEmit: Use tsc only for type checking, let bundler handle emit. Faster than tsc for both.const assertions: "hello" as const gives literal type "hello" (not string). Use for discriminant values, config objects.declare for ambient types: Declare module augmentations, global types, declare module "*.module.css". Reduces type-checking load.type EventMap = {
"order:created": { orderId: string; total: number };
"user:login": { userId: string; method: "email" | "oauth" };
"payment:failed": { orderId: string; reason: string; code: number };
};
class TypedEventBus {
private handlers = new Map<string, Set<Function>>();
on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void) {
if (!this.handlers.has(event as string)) {
this.handlers.set(event as string, new Set());
}
this.handlers.get(event as string)!.add(handler);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
this.handlers.get(event as string)?.forEach(h => h(data));
}
off<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void) {
this.handlers.get(event as string)?.delete(handler);
}
}
Hono is a TypeScript-first web framework for Cloudflare Workers, Deno, Bun, and Node.js. Benefits: ultralight (14KB), fast, native TypeScript with typed routes. Pattern:
import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const app = new Hono<{ Bindings: Env }>();
const OrderSchema = z.object({
customerId: z.number(),
items: z.array(z.object({ productId: z.string(), qty: z.number() })),
});
app.post('/api/orders', zValidator('json', OrderSchema), async (c) => {
const data = c.req.valid('json');
const order = await createOrder(data);
return c.json(order, 201);
});
Use for: serverless APIs, edge functions, middleware chains, WebSocket servers. Hono middleware: auth, CORS, rate limiting, JWT validation — all available as npm packages.
// Type-safe repository with CRUD operations
interface Entity {
id: string;
}
class Repository<T extends Entity> {
constructor(
private db: Database,
private table: string,
private schema: z.ZodType<T>,
) {}
async findById(id: string): Promise<T | null> {
const result = await this.db.query(
`SELECT * FROM ${this.table} WHERE id = $1`, [id]
);
if (!result) return null;
return this.schema.parse(result);
}
async create(data: Omit<T, 'id'>): Promise<T> {
const id = crypto.randomUUID();
const entity = { ...data, id } as T;
await this.db.insert(this.table, entity);
return entity;
}
async update(id: string, data: Partial<T>): Promise<T> {
const existing = await this.findById(id);
if (!existing) throw new NotFoundError(this.table, id);
const updated = this.schema.parse({ ...existing, ...data, id });
await this.db.update(this.table, updated);
return updated;
}
}
// Usage
const userRepo = new Repository(db, 'users', UserSchema);
const orderRepo = new Repository(db, 'orders', OrderSchema);
references/module-resolution.md — Module resolution strategies, ESM/CJSreferences/advanced-types-decorators.md — Conditional types, template literals, mapped types, decoratorsreferences/typescript-fundamentals.md — TypeScript Fundamentalsreferences/typescript-advanced.md — Advanced TypeScript Patternsreferences/typescript-zod.md — Zod Runtime Validation Guidemobile/react-native — React Native TypeScript patternsmobile/universal/testing — Vitest/Jest, type testingmobile/universal/networking — Type-safe API clientsmobile/universal/patterns — TypeScript patterns in mobile apps