围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。
记忆
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
| Situation | Play |
|---|---|
Value of unknown shape (API, JSON.parse, catch) | unknown + narrow before use; never any |
| Config/lookup object losing literal types | satisfies (TS >=4.9), not a type annotation |
| Union variant not narrowing | Add a literal discriminant field + exhaustive never check |
| 40-line "not assignable" error | Read 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 CI | errors.md |
| Generic won't infer / variance confusion / overload design | generics.md |
Partial/Omit/Record behaving oddly | utility-types.md |
| Untyped npm package, globals, augmentation, publishing types | declarations.md |
JS conversion, strict-flag rollout, as debt, TS version upgrade | migration.md |
| target/module/paths choices, monorepo, project references | tsconfig.md |
Green compile, runtime crash (x is not a function, ERR_REQUIRE_ESM) | modules.md |
| External data crossing a trust boundary | Validate at runtime at the boundary; static types only downstream → boundaries.md |
| tsc slow, editor lag, compiler out of memory | performance.md |
| Anything else typed | Default: 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
- Full
strictor you are reviewing half the code. Every flag off is a bug class the checker won't report. Greenfield:strict: trueday one plusnoUncheckedIndexedAccess(TS >=4.1). Legacy: stage flags with a ratchet (migration.md). unknownin, typed out. Data you didn't construct (network, JSON, env, storage) enters asunknownand gets validated once at the boundary (boundaries.md); past that point noanyand no re-checking.- 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.
- 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. - Every
ascarries 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. - Exhaustive by construction. Every
switchover a union ends indefault: 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. - Errors read bottom-up. The first lines of a long diagnostic are wrappers; the root cause is the deepest
Types of property ... are incompatibleline (errors.md).
Stop Using any
anyis viral both directions: it silences errors on reads AND poisons everything it's assigned to.unknownonly blocks reads until you narrow — same flexibility, none of the spreadcatch (e)isunknownunder strict since TS 4.4 (useUnknownInCatchVariables) — checke instanceof Errorbefore.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-eslintno-explicit-any+no-unsafe-*), record the baseline, fail CI when the count rises
Inference & Widening
Widening:
let x = "hello"isstring;const x = "hello"is"hello"— mutability drives widening- Object and array literals widen their members even under
const:const cfg = { mode: "dark" }hasmode: string—as constfreezes 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:
fnwith one explicit argument turns off inference for the rest — use the curried two-call patternmake()(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 wronginitialwidensTinstead 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
kindfield to each variant — narrowing and exhaustive switches come free (rules 4 and 6) - Discriminant must be a literal type — a field typed
stringdiscriminates nothing;as constthe 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 assumesxmay be reassigned — copy to aconstbefore the callback typeof x === "object"includesnull— checkx !== nullin the same conditionObject.keys(obj)returnsstring[], notkeyof typeof obj— intentional: structural types can carry extra keys. Cast only when you own the object literalArray.isArray()onunknownnarrows toany[]— element type needs its own checkinnarrows 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 onaction.kinddirectly
satisfies vs Annotation vs Cast
Three tools, three guarantees — pick by what you need to keep:
const x: T = v— checks AND widens toT: literal info gone, excess properties rejectedconst x = v satisfies T(TS >=4.9) — checks compatibility, keeps the inferred narrow type: the default for config/route/theme objectsv as T— checks nothing beyond rough overlap;"hello" as numberfails but{} as Usercompiles. It's an assertion, not a conversion
Strict Null Handling
??vs||:port || 3000turns a legitimate0into 3000;port ?? 3000only replacesnull/undefined. Same for""in string options- Optional chaining
?.producesundefined, nevernull— APIs contracted tonullneed 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]andrecord[key]becomeT | 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 itexactOptionalPropertyTypes(TS >=4.4) — distinguishes "absent" from "explicitly undefined"; breaks code that assignsundefinedto optional propsnoPropertyAccessFromIndexSignature,noImplicitOverride— cheap to adopt, low churnverbatimModuleSyntax(TS >=5.0) — forcesimport typefor type-only imports; the modern replacement forisolatedModulesimport 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
unknownor the concrete type - Slow type checking has structural causes and a measurement workflow —
performance.md
Output Gates
Before emitting TypeScript, verify:
- No
anyintroduced, explicit or implicit —unknownwhere the shape is genuinely open? - Exported functions annotated, parameters and return?
- Every
ashas a runtime guard above it or a justifying comment? - Every
switchover 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.
| Variable | Type | Default | Effect |
|---|---|---|---|
| runtime_target | node | browser | bun | deno | isomorphic | node | Drives module/moduleResolution and lib advice in tsconfig.md and import-extension rules in modules.md |
| project_kind | app | library | app | Library flips guidance to declaration emit, exports-map types, and wider TS-version support; app defaults to skipLibCheck and bundler resolution |
| compile_pipeline | tsc | transpile-only | transpile-only | transpile-only (esbuild, swc, Babel, bundlers) enforces the single-file-safe subset: no const enum, isolatedModules rules apply (modules.md); tsc lifts those bans |
| validation_library | zod | valibot | arktype | ajv | none | none | Names the schema library used in boundary-validation examples (boundaries.md); none keeps guidance library-neutral |
| ts_version | text (e.g. "5.4") | latest | Gates 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:
- conventions —
interfacevstypedefault, type naming style, banned features (enums, namespaces, decorators), type-only import style - strictness posture — which beyond-
strictflags 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
| Trap | Why it fails | Do instead |
|---|---|---|
enum | The one TS feature with runtime emit; const enum breaks under transpile-only compilers | as const object + keyof typeof (modules.md) |
Function or {} as a type | Function accepts any callable and calls return any; {} accepts everything except null/undefined | Exact signature (a: A) => R; Record for bags |
| Trusting excess property checks as validation | They fire only on fresh object literals — a value passed through a variable skips them | Runtime validation at boundaries (boundaries.md) |
// @ts-ignore | Keeps suppressing after the error is fixed, hiding the next one | // @ts-expect-error (TS >=3.9) — errors itself once stale |
! to silence strictNullChecks | Each one is an invisible cast and a crash candidate | Narrow, early-return, or ??; sanctioned only for framework-assigned class fields (migration.md) |
[key: string]: any to quiet index errors | Poisons every access on the object with any | Precise keys, Record, or a Map; noUncheckedIndexedAccess (TS >=4.1) keeps even honest signatures honest |
Where Experts Disagree
interfacevstype. Default:interfacefor object shapes (merges for augmentation, caches relationships for faster checking),typefor 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).
Related Skills
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
- If useful, star it: https://clawic.com/skills/typescript
- Latest version: https://clawic.com/skills/typescript
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.js 运行时、打包与线上问题,给出可落地的诊断依据与修复规则。
按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。
定位 Visual Studio Code 编辑器层的故障并修复:设置作用域、调试、格式化、扩展、快捷键、远程。
调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。
为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。