typescript-review

v2026.09.24

Reviewing TypeScript code - what to flag, and what the compiler and linter already flag for you USE WHEN: you are reviewing, critiquing or auditing existing TypeScript or TSX code - a "code review", a "review" of a .ts/.tsx file, a diff, a PR or a pull request; deciding what to comment on in TypeScript; avoiding false positives on typed JavaScript DO NOT USE FOR: writing, explaining or learning TypeScript - use `languages/typescript`; anything `tsc` under `strict` or a default typescript-eslint run already reports (this skill lists those so you can stay silent about them); React-specific review - pair with the React skills; lint rule configuration - use `quality/typescript-eslint`

GitHub
安装命令
npx skhub add claude-dev-suite/typescript-review
Markdown
SKILL.md

Reviewing TypeScript

A reviewer's leverage is what the toolchain cannot say. Every check below describes a defect that survives tsc --strict and a default typescript-eslint run.

TypeScript has a second trap Go does not: the compiler's strictness is a setting. Half of what follows depends on tsconfig.json, so read it before you comment - see the config-dependent section.

The snippets are fragments cut down to the defect, not runnable programs.

Already covered - do not spend review on it

DefectReported by
Unused local, unused parameternoUnusedLocals / noUnusedParameters, @typescript-eslint/no-unused-vars
Explicit any in a signature@typescript-eslint/no-explicit-any (recommended)
== where === belongseqeqeq
A promise created and never used@typescript-eslint/no-floating-promises - type-aware only, so absent unless the project enables type-checked linting
await on a non-thenable@typescript-eslint/await-thenable (type-aware)
Unreachable code after returnallowUnreachableCode: false
Missing return on some pathnoImplicitReturns
Implicit any parameternoImplicitAny (part of strict)

Check whether type-aware linting is on before assuming the promise rules ran: they need parserOptions.project, and many repos never enable it. If they are off, floating promises are worth reviewing by hand - that is a config finding plus a code finding, not one comment.

The table above dates; the tools do not. Verify a default set with mcp__documentation__fetch_docs on technology eslint — topics rules, typescript-eslint — and typescript/tsconfig for the compiler settings. Those entries point at the indexes the tool authors maintain, so they follow the version the project actually pins rather than the one this skill was written against.

The checks that earn their place

A type assertion that launders a lie

const user = JSON.parse(body) as User;   // no validation happened
const el = document.getElementById('x') as HTMLInputElement;  // may be null

When you see it: as T on a value crossing a trust boundary - a parsed payload, a DOM lookup, a fetch result, a database row.

Ask: what checked that this is actually a User? An assertion changes the type and nothing else; every downstream strict guarantee is now resting on it. The fix is a validator (zod, a type guard) or accepting unknown and narrowing.

no-explicit-any does not fire here - the code never says any, it says something worse: a specific type that is not true.

A type predicate that is not checked

function isUser(x: unknown): x is User {
  return typeof x === 'object' && x !== null;   // does not check a single field
}

When you see it: any function returning x is T.

Ask: does the body actually establish T? The compiler takes the predicate on faith - it verifies the return type is boolean, never that the check is sound. A wrong predicate is an assertion with extra steps, and it silently poisons every narrowed branch.

A non-null assertion standing in for a real check

const cfg = map.get(key)!;   // `!` says "trust me"; the map may not have it

When you see it: ! after an indexed access, a Map.get, a find, or an optional property.

Ask: what guarantees presence here? If it is an invariant, an explicit throw documents it and fails loudly; ! fails as undefined propagating into something far away. Distinguish the two cases: ! right after a has() check on the same key is fine, ! on a lookup whose key came from input is not.

Optional chaining that swallows the interesting case

const total = order?.items?.reduce(sum, 0) ?? 0;   // a missing order reads as 0

When you see it: ?. on a value the surrounding logic assumes exists, especially followed by ?? 0, ?? [] or ?? ''.

Ask: is absence genuinely equivalent to empty? A zero total for a missing order is not a safer answer than an error - it is a wrong answer that no longer looks wrong. ?. is right when absence is a real, expected state.

catch (e) narrowed by assumption

try { } catch (e) {
  logger.error(e.message);   // e is `unknown`; and anything can be thrown
}

When you see it: property access on a catch binding.

Ask: is this reachable with a non-Error throw? A rejected promise carrying a string, a throw from a library, a DOMException. Under useUnknownInCatchVariables (on with strict since 4.4) the compiler stops the direct access - but an as Error right after it is the same defect wearing a hat, and that compiles.

An await that was never written inside a callback

items.forEach(async (item) => {
  await save(item);          // forEach ignores the returned promise
});
done();                      // runs before any save finishes

When you see it: an async callback handed to forEach, map without Promise.all, or any API whose signature returns void.

Ask: does the caller need these to be finished? no-floating-promises does not fire, because forEach swallows the promise rather than the code discarding it. for...of with await, or Promise.all(items.map(...)), is the fix - and those two differ in concurrency, so pick deliberately.

Promise.all where partial failure is the normal case

const results = await Promise.all(userIds.map(fetchProfile));

When you see it: Promise.all over anything that can fail independently - network calls, per-tenant work, a fan-out over user input.

Ask: what should happen when one fails? all rejects on the first rejection, abandons the results of everything that succeeded, and leaves the other promises running unobserved. If partial success is meaningful, that is allSettled.

A shallow copy treated as a deep one

const next = { ...state };
next.filters.tags.push('new');   // mutates the original's nested array

When you see it: spread or Object.assign followed by a write to a nested property; readonly on a type whose members are objects.

Ask: is anything else holding the nested reference? readonly in TypeScript is one level deep and erased at runtime - it stops next.filters = ... and says nothing about next.filters.tags.push(...).

An index access typed as present

const first = rows[0];      // typed `Row`, not `Row | undefined`
first.id;                   // no error, throws at runtime on an empty array

When you see it: indexing an array or a Record<string, T> by a computed key, then using the result directly.

Ask: is noUncheckedIndexedAccess on? It is not part of strict, so in most projects this is invisible to the compiler and every array access is a claim that the index exists.

A discriminated union with no exhaustiveness guard

switch (action.type) {
  case 'add':    return add(action);
  case 'remove': return remove(action);
}   // a third variant added later returns undefined, silently

When you see it: a switch over a union discriminant with no default.

Ask: what happens when a variant is added? A default that assigns to never turns that future change into a compile error at exactly the right place. Without it the function's return type quietly becomes T | undefined.

Numeric sort left to the default comparator

[10, 9, 100].sort();   // [10, 100, 9] - compares as strings

When you see it: .sort() with no comparator on anything not already strings.

Ask: are these numbers? Nothing reports it; the signature is satisfied.

Config-dependent - read tsconfig.json before commenting

Half the checks above are only defects under a given configuration. Read the config the reviewed file actually resolves to - a monorepo has several, and extends chains matter.

SettingIn strict?What its absence means for the review
strict-Off: implicit any and unchecked null are everywhere; that is one architectural comment, not a per-line campaign
strictNullChecksyesOff: every nullability finding above is invisible to the compiler and worth reviewing by hand
useUnknownInCatchVariablesyes (4.4+)Off: catch (e) is any, so e.message compiles and the catch check applies
noUncheckedIndexedAccessnoOff (the common case): array and record access lie about presence
exactOptionalPropertyTypesnoOff: an optional property silently accepts an explicit undefined, which behaves differently from an absent key in Object.keys, spread and JSON
verbatimModuleSyntaxnoOff: a type-only import can survive into emitted JS and drag a runtime dependency with it

If the project sets skipLibCheck (most do), remember that nothing verified the .d.ts files a third-party type assertion is resting on.

What to say

Anchor the comment to the line, name the condition that triggers the defect, and say what breaks: "rows[0] is typed Row but noUncheckedIndexedAccess is off, so an empty result throws on .id here" beats "be careful with array access". If you cannot state the input that fails, it is a preference, not a defect.

State configuration findings once, at the top, not once per line they enable.

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/review/typescript

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1