Install
openclaw skills install @ivangdavila/typescriptWrites, reviews, and debugs type-safe TypeScript: type errors, narrowing, generics, tsconfig strictness, and declaration files. Use when a build hits type errors ("not assignable", "possibly undefined", "excessively deep", "cannot find module"), when eliminating any or unchecked as casts, designing generics or discriminated unions, writing .d.ts files or typing untyped npm packages, configuring tsconfig or module resolution, fixing ESM/CJS import crashes ("ERR_REQUIRE_ESM", "x is not a function"), validating API or JSON data at runtime, migrating JavaScript to TypeScript or upgrading the TypeScript version, or when tsc or the editor gets slow or runs out of memory.
openclaw skills install @ivangdavila/typescriptUser 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/.
.d.ts, module augmentation, publishing types)| 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.
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).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.{ 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.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.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.Types of property ... are incompatible line (errors.md).anyany 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 spreadcatch (e) is unknown under strict since TS 4.4 (useUnknownInCatchVariables) — check e instanceof Error before .messageas) gets either a runtime guard above it or a one-line comment saying why it's safe (rule 5)anys (typescript-eslint no-explicit-any + no-unsafe-*), record the baseline, fail CI when the count risesWidening:
let x = "hello" is string; const x = "hello" is "hello" — mutability drives wideningconst: const cfg = { mode: "dark" } has mode: string — as const freezes the whole tree (readonly + literals)<const T> type parameters (TS >=5.0) preserve them at the call siteInference limits:
fn<T, U> with one explicit argument turns off inference for the rest — use the curried two-call pattern make<T>()(config) to fix one and infer the otherNoInfer<T> (TS >=5.4) excludes a position from inference: declare function set<T>(vals: T[], initial: NoInfer<T>): void — without it, a wrong initial widens T instead of erroring(x) => without annotationModeling with unions:
kind field to each variant — narrowing and exhaustive switches come free (rules 4 and 6)string discriminates nothing; as const the source valuesNarrowing 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'tif (x), arr.map(() => x.foo) re-errors because TS assumes x may be reassigned — copy to a const before the callbacktypeof x === "object" includes null — check x !== null in the same conditionObject.keys(obj) returns string[], not keyof typeof obj — intentional: structural types can carry extra keys. Cast only when you own the object literalArray.isArray() on unknown narrows to any[] — element type needs its own checkin narrows unions only when the property appears in exactly one branch; on unlisted properties it narrows since TS 4.9const { kind } = action; if (kind === ...)) narrow only in TS >=4.6 — on older versions switch on action.kind directlysatisfies vs Annotation vs CastThree tools, three guarantees — pick by what you need to keep:
const x: T = v — checks AND widens to T: 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 number fails but {} as User compiles. It's an assertion, not a conversion?? vs ||: port || 3000 turns a legitimate 0 into 3000; port ?? 3000 only replaces null/undefined. Same for "" in string options?. produces undefined, never null — APIs contracted to null need an explicit ?? null! is a cast in disguise — prefer early return or narrowing; each ! is a runtime crash candidate the compiler can no longer seestrictstrict: 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 itexactOptionalPropertyTypes (TS >=4.4) — distinguishes "absent" from "explicitly undefined"; breaks code that assigns undefined to optional propsnoPropertyAccessFromIndexSignature, noImplicitOverride — cheap to adopt, low churnverbatimModuleSyntax (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.
isolatedDeclarations (TS >=5.5) and faster declaration emitunknown or the concrete typeperformance.mdBefore emitting TypeScript, verify:
any introduced, explicit or implicit — unknown where the shape is genuinely open?as has a runtime guard above it or a justifying comment?switch over a union ends in an exhaustiveness check?! where narrowing or ?? would prove it?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:
interface vs type default, type naming style, banned features (enums, namespaces, decorators), type-only import stylestrict flags to push proactively, cast tolerance, error-suppression policy| 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<string, unknown> 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<K, V>, or a Map; noUncheckedIndexedAccess (TS >=4.1) keeps even honest signatures honest |
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.isolatedDeclarations (TS >=5.5), where emit requires them anyway.boundaries.md).More Clawic skills, get them at https://clawic.com/skills/typescript (install if the user confirms):
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/typescript.