记忆

TypeScript

解读 TypeScript 类型错误,设计 API、tsconfig 与 .d.ts 的类型方案。

它能做什么

按自下而上方式读 TypeScript 诊断信息,套用一整套严类型规则:信任边界用 `unknown`、用可辨识联合体替代多 optional 字段、配置对象用 `satisfies`、导出函数标注签名内部推断、switch 用 `never` 做穷尽检查。覆盖类型错误解读(not assignable、TS2589、TS2742)、泛型推断与重载设计、`strict` 之外的 tsconfig 开关、.d.ts 文件编写、ESM/CJS 互操作修复,以及 tsc 与编辑器性能问题。每个方向的详细打法写在对应的伴生文档里,用户偏好从本地配置文件加载。

什么时候用它

  • CI 上冒出一长串 "not assignable",本地复现不出来,也读不出根因
  • 用 `{ data?, error? }` 写了业务状态,想换成可辨识联合体消灭非法状态
  • 给一个没有类型的 npm 包写 .d.ts,或准备随包发布类型
  • 老项目开启 noUncheckedIndexedAccess、exactOptionalPropertyTypes 这类 `strict` 之外的开关,需要分阶段推进

技能文档

User preferences and memory live in ~/Clawic/data/typescript/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/typescript/ or ~/clawic/typescript/), move it to ~/Clawic/data/typescript/.

When To Use

  • Decoding type errors: "not assignable" walls, narrowing failures, inference surprises, errors that appear only in CI
  • Designing types for an API surface: generics, discriminated unions, utility types, branded types
  • tsconfig decisions: strict flags, module resolution, target/lib, monorepo project references, JS-to-TS migration order
  • Writing or fixing declaration files (.d.ts, module augmentation, publishing types)
  • Code that compiles green but fails at runtime — ESM/CJS interop crashes, unvalidated external data — or tsc/editor slowness
  • Not for runtime JavaScript semantics (closures, event loop, coercion) — that is the javascript skill

Quick Reference

SituationPlay
Value of unknown shape (API, JSON.parse, catch)unknown + narrow before use; never any
Config/lookup object losing literal typessatisfies (TS >=4.9), not a type annotation
Union variant not narrowingAdd a literal discriminant field + exhaustive never check
40-line "not assignable" errorRead the deepest Types of property ... are incompatible line first — root cause is at the bottom; full decoder → errors.md
Cryptic diagnostic (TS2589, TS2742, "no overload matches"), error only in CIerrors.md
Generic won't infer / variance confusion / overload designgenerics.md
Partial/Omit/Record behaving oddlyutility-types.md
Untyped npm package, globals, augmentation, publishing typesdeclarations.md
JS conversion, strict-flag rollout, as debt, TS version upgrademigration.md
target/module/paths choices, monorepo, project referencestsconfig.md
Green compile, runtime crash (x is not a function, ERR_REQUIRE_ESM)modules.md
External data crossing a trust boundaryValidate at runtime at the boundary; static types only downstream → boundaries.md
tsc slow, editor lag, compiler out of memoryperformance.md
Anything else typedDefault: full strict, infer locals, annotate exports

Depth on demand: errors.md diagnostic decoding · generics.md inference, variance, conditional types, overloads · utility-types.md built-in type traps · declarations.md .d.ts, augmentation, publishing · migration.md JS-to-TS, strictness ratchets, TS upgrades · tsconfig.md compiler configuration · modules.md ESM/CJS interop · boundaries.md runtime validation · performance.md compile and editor speed.

Core Rules

  1. Full strict or you are reviewing half the code. Every flag off is a bug class the checker won't report. Greenfield: strict: true day one plus noUncheckedIndexedAccess (TS >=4.1). Legacy: stage flags with a ratchet (migration.md).
  2. unknown in, typed out. Data you didn't construct (network, JSON, env, storage) enters as unknown and gets validated once at the boundary (boundaries.md); past that point no any and no re-checking.
  3. Annotate exports, infer locals. Annotated parameters and return on every exported function put errors at the boundary that caused them, not at 30 call sites downstream; inference inside bodies keeps the noise down.
  4. Make illegal states unrepresentable. n independent optional fields admit 2^n combinations: { data?: T; error?: E } is 4 states of which 2 are illegal. A discriminated union with one variant per legal state encodes exactly the truth.
  5. Every as carries a proof. A cast overrides the checker — attach the runtime guard that makes it true, or a one-line comment saying why it cannot be wrong. An unexplained cast is a deferred bug report.
  6. Exhaustive by construction. Every switch over a union ends in default: x satisfies never (TS >=4.9) — adding a variant then becomes a compile error at every site that must handle it, which is the entire point of the union.
  7. Errors read bottom-up. The first lines of a long diagnostic are wrappers; the root cause is the deepest Types of property ... are incompatible line (errors.md).

Stop Using any

  • any is viral both directions: it silences errors on reads AND poisons everything it's assigned to. unknown only blocks reads until you narrow — same flexibility, none of the spread
  • catch (e) is unknown under strict since TS 4.4 (useUnknownInCatchVariables) — check e instanceof Error before .message
  • Every remaining cast (as) gets either a runtime guard above it or a one-line comment saying why it's safe (rule 5)
  • Ratchet, don't boil the ocean: count anys (typescript-eslint no-explicit-any + no-unsafe-*), record the baseline, fail CI when the count rises

Inference & Widening

Widening:

  • let x = "hello" is string; const x = "hello" is "hello" — mutability drives widening
  • Object and array literals widen their members even under const: const cfg = { mode: "dark" } has mode: stringas const freezes the whole tree (readonly + literals)
  • Literal arguments: primitives infer as literals, objects/arrays widen — `` type parameters (TS >=5.0) preserve them at the call site
  • Function return types widen too — annotate the return when a caller needs the literal

Inference limits:

  • Inference flows from arguments to parameters, not through intermediate generics — when a nested generic fails, name the intermediate type and split into two steps
  • No partial type argument inference: fn with one explicit argument turns off inference for the rest — use the curried two-call pattern make()(config) to fix one and infer the other
  • NoInfer (TS >=5.4) excludes a position from inference: declare function set(vals: T[], initial: NoInfer): void — without it, a wrong initial widens T instead of erroring
  • Callback parameters get contextual types only when the expected signature is known — assigning the function to a typed variable or parameter is what enables (x) => without annotation

Discriminated Unions & Narrowing

Modeling with unions:

  • Add a literal kind field to each variant — narrowing and exhaustive switches come free (rules 4 and 6)
  • Discriminant must be a literal type — a field typed string discriminates nothing; as const the source values

Narrowing failures:

  • filter(Boolean) doesn't narrow in any TS version — write .filter((x): x is T => Boolean(x)). TS >=5.5 infers predicates from simple lambdas, so .filter(x => x != null) narrows there; below 5.5 it doesn't
  • Narrowing dies inside callbacks: after if (x), arr.map(() => x.foo) re-errors because TS assumes x may be reassigned — copy to a const before the callback
  • typeof x === "object" includes null — check x !== null in the same condition
  • Object.keys(obj) returns string[], not keyof typeof obj — intentional: structural types can carry extra keys. Cast only when you own the object literal
  • Array.isArray() on unknown narrows to any[] — element type needs its own check
  • in narrows unions only when the property appears in exactly one branch; on unlisted properties it narrows since TS 4.9
  • Destructured discriminants (const { kind } = action; if (kind === ...)) narrow only in TS >=4.6 — on older versions switch on action.kind directly

satisfies vs Annotation vs Cast

Three tools, three guarantees — pick by what you need to keep:

  • const x: T = v — checks AND widens to T: literal info gone, excess properties rejected
  • const x = v satisfies T (TS >=4.9) — checks compatibility, keeps the inferred narrow type: the default for config/route/theme objects
  • v as T — checks nothing beyond rough overlap; "hello" as number fails but {} as User compiles. It's an assertion, not a conversion

Strict Null Handling

  • ?? vs ||: port || 3000 turns a legitimate 0 into 3000; port ?? 3000 only replaces null/undefined. Same for "" in string options
  • Optional chaining ?. produces undefined, never null — APIs contracted to null need an explicit ?? null
  • Non-null ! is a cast in disguise — prefer early return or narrowing; each ! is a runtime crash candidate the compiler can no longer see

Strictness Beyond strict

strict: true is not the strictest configuration. Flags it does NOT include:

  • noUncheckedIndexedAccess (TS >=4.1) — arr[i] and record[key] become T | undefined. Catches the most common real crash (indexing off the end); enable on greenfield day one, on legacy expect a large error wave and stage it
  • exactOptionalPropertyTypes (TS >=4.4) — distinguishes "absent" from "explicitly undefined"; breaks code that assigns undefined to optional props
  • noPropertyAccessFromIndexSignature, noImplicitOverride — cheap to adopt, low churn
  • verbatimModuleSyntax (TS >=5.0) — forces import type for type-only imports; the modern replacement for isolatedModules import hygiene (modules.md)

Everything else about compiler configuration — target/lib, module × moduleResolution, paths, monorepos — lives in tsconfig.md; import/export mechanics and transpile-only compatibility in modules.md.

Where To Annotate

  • Default: annotate exported function signatures (parameters AND return), infer everything inside bodies (rule 3)
  • Explicit return types on the public surface also unlock isolatedDeclarations (TS >=5.5) and faster declaration emit
  • A type parameter must relate at least two positions (two params, or param and return). Used once, it's decoration — replace with unknown or the concrete type
  • Slow type checking has structural causes and a measurement workflow — performance.md

Output Gates

Before emitting TypeScript, verify:

  • No any introduced, explicit or implicit — unknown where the shape is genuinely open?
  • Exported functions annotated, parameters and return?
  • Every as has a runtime guard above it or a justifying comment?
  • Every switch over a union ends in an exhaustiveness check?
  • External data validated at runtime before its first typed use?
  • No ! where narrowing or ?? would prove it?

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/typescript/config.yaml.

VariableTypeDefaultEffect
runtime_targetnode | browser | bun | deno | isomorphicnodeDrives module/moduleResolution and lib advice in tsconfig.md and import-extension rules in modules.md
project_kindapp | libraryappLibrary flips guidance to declaration emit, exports-map types, and wider TS-version support; app defaults to skipLibCheck and bundler resolution
compile_pipelinetsc | transpile-onlytranspile-onlytranspile-only (esbuild, swc, Babel, bundlers) enforces the single-file-safe subset: no const enum, isolatedModules rules apply (modules.md); tsc lifts those bans
validation_libraryzod | valibot | arktype | ajv | nonenoneNames the schema library used in boundary-validation examples (boundaries.md); none keeps guidance library-neutral
ts_versiontext (e.g. "5.4")latestGates every TS >=X.Y-marked recommendation in this skill: below the mark, the pre-version workaround is offered instead (e.g. below 4.9 an annotation replaces satisfies)

Preference areas to record as the user reveals them:

  • conventionsinterface vs type default, type naming style, banned features (enums, namespaces, decorators), type-only import style
  • strictness posture — which beyond-strict flags to push proactively, cast tolerance, error-suppression policy
  • tooling — package manager, typescript-eslint strictness, monorepo layout (project references vs single config), type-test tooling
  • output format — quick fix vs explained fix, whether to show before/after diffs

Traps

TrapWhy it failsDo instead
enumThe one TS feature with runtime emit; const enum breaks under transpile-only compilersas const object + keyof typeof (modules.md)
Function or {} as a typeFunction accepts any callable and calls return any; {} accepts everything except null/undefinedExact signature (a: A) => R; Record for bags
Trusting excess property checks as validationThey fire only on fresh object literals — a value passed through a variable skips themRuntime validation at boundaries (boundaries.md)
// @ts-ignoreKeeps suppressing after the error is fixed, hiding the next one// @ts-expect-error (TS >=3.9) — errors itself once stale
! to silence strictNullChecksEach one is an invisible cast and a crash candidateNarrow, early-return, or ??; sanctioned only for framework-assigned class fields (migration.md)
[key: string]: any to quiet index errorsPoisons every access on the object with anyPrecise keys, Record, or a Map; noUncheckedIndexedAccess (TS >=4.1) keeps even honest signatures honest

Where Experts Disagree

  • interface vs type. Default: interface for object shapes (merges for augmentation, caches relationships for faster checking), type for unions, functions, tuples, and mapped/conditional results. An existing codebase convention beats either.
  • Explicit return types everywhere vs boundary-only. Boundary-only is the default (rule 3). The everywhere school wins on published libraries and under isolatedDeclarations (TS >=5.5), where emit requires them anyway.
  • Runtime validation depth. Edges-only is the default — validating internally recomputes what the checker already proved. The schema-first school (schema as source of truth, static types inferred from it) wins when one shape crosses many boundaries (boundaries.md).

More Clawic skills, get them at https://clawic.com/skills/typescript (install if the user confirms):

  • javascript — runtime semantics: coercion, closures, event loop
  • react — typing components, hooks, and props in practice
  • nodejs — Node runtime, packaging, and ESM/CJS beyond the type layer
  • deno — TypeScript-native runtime without a build step

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/typescript.

常见问题

它会帮我修运行时的 JS bug 吗,比如闭包、事件循环、隐式转换?
不在范围内。文档明确写了这块由另一个 JavaScript 方向的技能负责。本技能只看类型系统、编译配置,以及由类型问题引发的运行时故障(ESM/CJS 互操作、未校验的外部数据等)。
默认按哪个 TypeScript 版本来给建议?
默认按最新版。文档中凡是标注了 `TS >= X.Y` 的建议,会按用户配置的 `ts_version` 自动回退到旧版本的写法,比如 4.9 之前的项目把 `satisfies` 换回类型注解或断言。
能继续放心用 `any` 吗?
不建议。技能会推动 `unknown` + 收窄,把 `any` 视为会传染的东西:读取时静默错误,赋出去又会污染下游。建议接入 typescript-eslint 统计 `any` 数量,在 CI 里禁止上涨。

相关技能

围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。

作者 Iván135 次安装6 星标

针对 Node.js 运行时、打包与线上问题,给出可落地的诊断依据与修复规则。

142 次安装5 星标

按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。

245 次安装3 星标

定位 Visual Studio Code 编辑器层的故障并修复:设置作用域、调试、格式化、扩展、快捷键、远程。

103 次安装3 星标

调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。

82 次安装3 星标

为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。

101 次安装2 星标