Claude Cursor GitHub Copilot Skill

typescript-contracts-review

Review TypeScript diffs and tsconfig strictness posture for sound type contracts — auditing any/assertion usage at trust boundaries, unsound narrowing, and exported public-API type-surface breakage — so that a passing compile is meaningful evidence rather than a decorative pass,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download vincentchuwaichow-vanguard-frontier-agentic-skills_frontend_typescript-contracts-review-febe32a.zip · 14 KB
Part of vincentchuwaichow/vanguard-frontier-agentic — 293 skills

Install

skills CLI npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/typescript-contracts-review
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
Git git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

TypeScript Contracts Review

Purpose

"The build passes" is only meaningful evidence of type safety if the active tsconfig is actually strict and the code doesn't defeat it with any, unchecked assertions, or broad suppression comments — and TypeScript types are fully erased at compile time, so a type annotation on external data (a parsed JSON response, a third-party SDK payload, a URL parameter) provides zero runtime protection unless paired with an actual runtime validator. This skill exists so those two failure modes — a loose or silently-weakened tsconfig, and type annotations that assert safety no runtime check backs up — get audited explicitly instead of being assumed away by a green checkmark. It also checks exported public-API type surfaces so a "just an internal refactor" diff doesn't silently break every downstream consumer of a published package.

When to use

Use this skill when the user asks to:

  • review a TypeScript/TSX diff for type-safety soundness before merge,
  • audit a codebase's tsconfig strictness posture against current TypeScript-recommended defaults,
  • check any/type-assertion/non-null-assertion usage, especially at external-data boundaries,
  • verify a discriminated union or type-guard function actually narrows correctly,
  • assess whether a change to an exported/published package breaks its public API type surface.

Do not use this skill for:

  • pure runtime/logic bug hunting that has nothing to do with type contracts — that is a general code-review task,
  • JavaScript files with no type annotations or JSDoc types — there is no type contract to audit,
  • live tsc --noEmit/type-coverage execution results interpretation beyond what this static review can determine from the diff — report that a live run is needed rather than fabricating its result.

Context7 Documentation Protocol

  • Resolve the TypeScript library ID with resolve-library-id before ruling on any compiler-flag or strict-family question; do not answer from memory, because recommended defaults change across releases (confirmed via Context7: TypeScript 5.9's tsc --init now emits noUncheckedIndexedAccess: true and exactOptionalPropertyTypes: true as a separate "Stricter Typechecking Options" block alongside strict: true, where earlier tsc --init output only set strict: true).
  • Before asserting which individual flags strict: true bundles (strictNullChecks, noImplicitAny, noImplicitThis, alwaysStrict, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, useUnknownInCatchVariables), call query-docs against the current TypeScript docs rather than reciting a memorized list — the bundle is described as open-ended ("future versions of TypeScript may introduce additional stricter checking under this flag"), so a stale list under-reports what a repo's strict: true actually enables on its installed compiler version.
  • Before flagging or endorsing a typescript-eslint rule (e.g. no-explicit-any, no-unsafe-assignment, no-non-null-assertion), verify via query-docs whether that rule is in the repo's active shared config (recommended, recommended-type-checked, strict-type-checked) — rule membership across those tiers has changed between major versions (confirmed via Context7: the v7→v8 recommended-type-checked diff added/removed multiple rules), so do not assume a rule is enabled just because the repo extends a config by name without checking the installed version.
  • Read package.json/lockfile first to confirm the installed typescript and typescript-eslint/@typescript-eslint/* major versions before citing version-gated behavior (e.g. exactOptionalPropertyTypes semantics, satisfies operator availability, const type parameters) — do not assume a feature is available because it appears in current docs if the installed major predates it.
  • If Context7 is unavailable, fall back to the official_docs URLs in this skill's metadata.json and label every version-sensitive claim documentation-based, unverified against installed compiler version.

Lean operating rules

  • Never accept "the build passes" as sufficient evidence of type safety without checking the actual tsconfig strict-family flags in effect — a loose config proves far less than developers assume, and strict: true alone (pre-5.9 tsc --init default) does not imply noUncheckedIndexedAccess or exactOptionalPropertyTypes are on.
  • Every new any must carry an adjacent justification comment; flag unjustified any as blocking, especially inside application logic that later consumers trust as validated.
  • Every trust-boundary type (parsed JSON, third-party SDK response, postMessage payload, URL/query-param, form input, environment variable) must be paired with an actual runtime validator — a type annotation alone is erased at compile time and enforces nothing at runtime.
  • Treat any proposal to loosen existing tsconfig strictness (removing strict, disabling strictNullChecks) as requiring an explicit, separately-reviewed migration plan, not a routine PR change.
  • Flag broad @ts-nocheck/file-level suppression as blocking by default; require @ts-ignore/@ts-expect-error to carry an adjacent comment explaining why, weighted higher severity if the suppressed code is security- or trust-boundary-relevant.
  • Do not let generic-type complexity become unreadable; a type requiring a comment to explain what it constrains is a design smell worth simplifying, not a badge of sophistication.
  • Do not run or assert tsc --noEmit success from memory — invoke it or explicitly flag that CI must, rather than fabricating a compile-success claim.
  • When a diff touches an exported/published package's public surface, check for a breaking type change (removed export, narrowed parameter type, widened return type becoming a narrower consumer-facing type, added required property) before approving; a type-only change can still be a semver-breaking change even with zero runtime behavior difference.

References

Load these only when needed:

  • Strict-flag posture reference — use when auditing or recommending a tsconfig strict-family flag set, including which flags are bundled under strict vs. separately opt-in (noUncheckedIndexedAccess, exactOptionalPropertyTypes), and how to read a repo's actual effective config (including extends chains).
  • Trust-boundary validation patterns — use when auditing external-data ingestion points (API responses, JSON.parse, postMessage, URL parsing, form/env input) for paired runtime validation against their declared types.
  • Public API surface diffing — use when a diff touches an exported/published package and a breaking-change check against the previous public .d.ts/export surface is needed.

Response minimum

Return, at minimum:

  • the tsconfig strictness posture summary (which strict-family flags are on/off vs. current TypeScript-recommended defaults, and the compiler version that posture was checked against),
  • the any/assertion audit for every new any, as, and ! in the diff, each flagged with its justification or lack thereof and file:line evidence,
  • the trust-boundary validation audit for every external-data ingestion point touched, naming the missing or present runtime validator,
  • the public-API surface diff and any breaking-change flags, when the diff touches an exported package,
  • verdict (approve / approve-with-notes / block),
  • residual risk notes for anything requiring a live tsc --noEmit/type-coverage run beyond this static diff review.
Files (vanguard-frontier-agentic)
  • references
    • public-api-surface-diff.md 6.9 KB
      # Public API Surface Diffing
      
      Use this reference when a diff touches an exported/published package (a library consumed by other packages or external users via its declared entry points and generated `.d.ts` files) and a breaking-change check against the previous public type surface is needed.
      
      ## What people get wrong
      
      The naive story is:
      
      > No runtime logic changed, only type signatures — so this can't be a breaking change.
      
      Wrong. TypeScript's declaration-file contract (`.d.ts`) *is* the public API surface for a published package's consumers. A change with zero runtime behavior difference can still be a semver-breaking change for anyone whose code type-checks against this package, because their build breaks even though the JavaScript that ships would have behaved identically.
      
      ## Officially grounded shape
      
      Per the TypeScript declaration-files handbook, `.d.ts` files describe the shape of a library to consumers — they are the contract a downstream `tsc` run checks against, independent of the actual runtime implementation. Consequences that matter for a breaking-change review:
      
      - **Removing an export** (a function, type, interface, class, or re-export) breaks every consumer importing it, even if nothing else changed.
      - **Narrowing a parameter type** (accepting fewer input shapes than before, e.g. `string` → `'a' | 'b'`) breaks consumers who were legitimately passing a now-disallowed value, even if that value never caused a runtime error.
      - **Widening a publicly-returned type in a way that removes previously-guaranteed members** (e.g. a return type that used to always include `id: string` now types it as `id?: string`, or a return type widens from a specific union to a broader one) breaks consumers whose downstream code relied on the narrower guarantee, even though the runtime object may not have changed at all.
      - **Adding a required property to an exported interface/type** that consumers are expected to construct (not just consume) breaks every call site constructing that type without the new property.
      - **Changing a type from an interface to a type alias (or vice versa)** is usually safe for consumption but can break consumers who were declaration-merging against the interface — a real but narrower risk to check when the package is designed for extension.
      - **Generic parameter changes** (adding a required type parameter, changing a default, changing variance-relevant usage) can break consumers who don't explicitly specify type arguments and were relying on inference.
      
      ## Non-negotiable design rules
      
      1. **Diff the emitted public surface, not just the source diff.** The source diff shows what changed in the implementation file; the question that matters is what changed in what the package actually exports from its declared entry point(s) (per `package.json` `exports`/`types`/`main` fields). An internal type change that never reaches an exported symbol is not a public API break.
      2. **Type-only changes are real changes.** Do not wave through a diff as "just types, no behavior change" — for a published package, a type-only change to an exported symbol is exactly the kind of change semver-breaking-change review exists for.
      3. **Distinguish parameter-position from return-position changes** — narrowing accepted input is breaking; narrowing *offered* output guarantees is also breaking, but for a different reason (consumers relying on the wider prior contract). Both directions matter; do not only check one.
      4. **Check re-exports, not just direct declarations.** A type re-exported from a barrel file (`export * from './internal'`) is part of the public surface exactly as much as a type declared at the entry point — a change to the underlying `./internal` type is a public break even if the entry-point file itself has no diff.
      5. **A change behind an internal-only export or a `@internal`/unexported symbol is not a public break** — confirm the changed symbol is actually reachable from the package's declared public entry point(s) before flagging it as breaking; do not over-flag purely-internal refactors.
      
      ## Minimal safe audit flow
      
      1. Identify the package's declared public entry point(s) from `package.json` (`main`, `module`, `types`/`typings`, or the `exports` map's `types` conditions for each subpath).
      2. From the diff, list every exported symbol (function, class, interface, type alias, enum, const) whose declaration changed, was added, or was removed — including via re-export chains reachable from the entry point(s).
      3. For each changed exported symbol, classify the change: added (usually safe), removed (breaking), parameter narrowed (breaking for callers), parameter widened (usually safe), return narrowed/property-removed-or-optionalized (breaking for consumers), return widened purely additively (usually safe).
      4. For anything classified breaking, state the concrete failure mode for a consumer: what specific call pattern that currently type-checks against the published types would fail to type-check against the new ones.
      5. If the repo has no automated API-surface-diff tooling in place (e.g. no `api-extractor`, no `.d.ts`-snapshot test), note that as a process gap rather than silently accepting the manual audit as a full substitute for automated coverage on every future change.
      
      ## High-risk assumptions to kill
      
      - "No runtime behavior changed, so it's not breaking" — irrelevant for a published package's type contract.
      - "This is just a refactor, the public API is the same" — verify against the actual entry-point re-export chain; an internal rename can accidentally change what a barrel file re-exports.
      - "Adding a property to an interface is always additive/safe" — true for consumption (reading), false for construction (a consumer building an object literal to satisfy that interface now needs the new property too, unless it was added as optional).
      - "TypeScript would catch this for us automatically" — it catches it for *this repo's own* compile, not for downstream consumers' separate compiles against the published `.d.ts`; the break only surfaces in their build, after the package is published.
      
      ## Verification targets
      
      - `package.json` `exports`/`types`/`main` fields, to establish the actual public entry point(s).
      - `git diff` scoped to files reachable from those entry points (direct declarations and re-export chains).
      - If available, a generated `.d.ts` diff (build output before/after) or an `api-extractor`-style report — prefer this over manual source-diff reasoning when the tooling exists, since re-export chains and inferred types are easy to miscount by hand.
      
      ## When to push back
      
      Push back if the user asks to:
      
      - ship a narrowed parameter type or removed export as a patch/minor version bump without flagging it as a breaking (major) change,
      - describe a type-only breaking change as "not a real change" because runtime behavior is unaffected,
      - skip the public-surface check because "it's just an internal refactor" without first confirming the changed symbol isn't reachable from a declared entry point.
      
    • strict-flag-posture.md 7.3 KB
      # Strict-Flag Posture
      
      Use this reference when auditing or recommending a tsconfig strict-family flag set, or when a reviewer needs to determine what a repo's `strict: true` actually enables on its installed compiler.
      
      ## What people get wrong
      
      The naive story is:
      
      > The repo sets `"strict": true`, so it's using TypeScript's strictest checking.
      
      Incomplete, for two separate reasons:
      
      1. `strict` is a bundle, and the bundle's membership is **open-ended by design**. Per the TypeScript docs: turning `strict` on "is equivalent to enabling all of the strict mode family options... Future versions of TypeScript may introduce additional stricter checking under this flag." A repo pinned to an older `typescript` version gets a smaller bundle than the same `strict: true` on a newer one.
      2. Two of the most consequential type-soundness flags — `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` — are **not** part of the `strict` bundle at all. They are separate opt-in flags. A repo can be maximally "strict" by the `strict: true` definition and still have neither enabled.
      
      ## Officially grounded shape (what the docs say)
      
      - `strict: true` is the master flag; it currently bundles at minimum `strictNullChecks`, `noImplicitAny`, `noImplicitThis`, `alwaysStrict`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, and (since TypeScript 4.4) `useUnknownInCatchVariables`. Verify the current exact bundle via `query-docs` against the installed version — do not treat this list as closed or version-independent.
      - `useUnknownInCatchVariables` (TS 4.4+) changes the default type of a `catch` variable from `any` to `unknown`. It is auto-enabled under `strict`. Its presence means catch-block property access (`err.message`) requires narrowing first (e.g. `instanceof Error`) — code that skips narrowing under an older compiler target may newly fail, or may be silently unsound if the repo is still pre-4.4.
      - `noUncheckedIndexedAccess` — **not** in the `strict` bundle — makes indexed access (`arr[i]`, `record[key]`) include `undefined` in the result type. Without it, index signatures and array access lie: `arr[i]` types as `T`, not `T | undefined`, even though out-of-bounds access returns `undefined` at runtime.
      - `exactOptionalPropertyTypes` — **not** in the `strict` bundle — makes `{ prop?: string }` distinguish "prop is absent" from "prop is present and set to `undefined`". Without it, `obj.prop = undefined` is accepted as satisfying an optional property even where the author's intent required the key to be entirely absent (relevant for APIs that branch on `'prop' in obj` or `Object.keys`).
      - As of TypeScript 5.9, `tsc --init` generates both of these under a separate "Stricter Typechecking Options" block alongside `strict: true` — this is a **default-generation change**, not a retroactive change to what `strict` means. Repos scaffolded before 5.9 will not have picked these up automatically, and repos on TypeScript versions predating their introduction cannot have them regardless of `tsc --init` vintage.
      
      > Version note: exact strict-family bundle membership and default `tsc --init` output are version-sensitive. Confirm both via `query-docs` against the specific `typescript` version in the repo's lockfile before making a definitive claim — do not rely on a memorized flag list.
      
      ## Non-negotiable design rules
      
      1. **Read the effective config, not just the top-level file.** `tsconfig.json` can `extends` a base config (monorepo shared config, `@tsconfig/*` package). A flag absent from the leaf file may be inherited, or a permissive leaf-level override may silently defeat a strict base. Resolve the full `extends` chain before concluding a flag is off.
      2. **Distinguish "not in `strict`" from "not needed."** `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` are the two flags most commonly mistaken for redundant with `strict`. Treat their absence as a real gap to call out, not a non-finding, whenever the diff under review does array/record indexing or optional-property assignment that would behave differently under them.
      3. **Version-gate every strict-bundle claim.** Do not assert "this repo has `useUnknownInCatchVariables`" from `strict: true` alone — confirm the installed `typescript` version is 4.4+ first.
      4. **Treat strictness removal as a migration, not a diff line.** A one-line change disabling `strictNullChecks` or removing `strict` can silently make thousands of previously-checked call sites unchecked. Require an explicit, separately-reviewed migration plan (with a scoped rollout, e.g. per-directory `strict` via TypeScript project references, or documented follow-up) rather than approving it inline in an unrelated feature PR.
      5. **Do not conflate ESLint strictness with compiler strictness.** `typescript-eslint`'s `strict-type-checked` config and the compiler's `strict` flag are independent axes — a repo can have one without the other. Check both when the review scope includes lint config.
      
      ## Minimal safe audit flow
      
      1. Read `package.json`/lockfile to get the exact `typescript` version.
      2. Read `tsconfig.json` and resolve its full `extends` chain to the effective merged config.
      3. List which strict-family flags (per the version-confirmed bundle) are on, and separately whether `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` are on.
      4. Compare against current TypeScript-recommended defaults (via `query-docs`) for that version, and note the gap explicitly rather than treating "has `strict: true`" as sufficient.
      5. If the diff under review does indexed access or optional-property assignment and the relevant flag is off, name the specific unsound pattern this enables (e.g. "line 42 assumes `config[key]` is never `undefined`; with `noUncheckedIndexedAccess` off, the compiler will not catch this if `key` is absent from `config`").
      
      ## High-risk assumptions to kill
      
      - "`strict: true` means fully strict" — it does not include `noUncheckedIndexedAccess` or `exactOptionalPropertyTypes`.
      - "The build passes, so the types are sound" — passes only prove soundness relative to whatever flags are actually active, which may be a materially weaker set than the reviewer assumes.
      - "Newer TypeScript version means newer defaults apply automatically" — `tsc --init` default generation only affects newly scaffolded configs; upgrading the compiler does not retroactively add opt-in flags to an existing `tsconfig.json`.
      - "This monorepo's shared base config is strict, so every package is strict" — a leaf `tsconfig.json` can override or narrow it; always resolve the effective chain.
      
      ## Verification targets
      
      - `cat tsconfig.json` and every file in its `extends` chain.
      - `npm ls typescript` / lockfile entry for the resolved `typescript` version.
      - `tsc --showConfig` (if available in the environment) to print the fully resolved effective compiler options — prefer this over manually merging `extends` chains by hand when the tool is available.
      
      ## When to push back
      
      Push back if the user asks to:
      
      - disable `strict` (or any of its family) as a quick fix to unblock a build, without a scoped migration plan,
      - treat a passing `tsc --noEmit` as proof of runtime safety for external-data handling — it is not; that is the trust-boundary concern in a separate reference,
      - skip checking `noUncheckedIndexedAccess`/`exactOptionalPropertyTypes` because "the repo has `strict: true`" — that claim alone does not cover those flags.
      
    • trust-boundary-validation.md 8.6 KB
      # Trust-Boundary Validation Patterns
      
      Use this reference when auditing external-data ingestion points — API responses, `JSON.parse`, `postMessage`, URL/query-param parsing, form input, environment variables — for whether a declared TypeScript type is actually backed by a runtime check.
      
      ## What people get wrong
      
      The naive story is:
      
      > I typed the response as `User`, so `response.data` is a `User`.
      
      Wrong, and dangerously so. TypeScript types are fully erased at compile time — the emitted JavaScript contains no trace of the type annotation. A cast like `const user = response.data as User` or a generic call like `fetch<User>(url)` where the generic only annotates the return type performs **zero runtime work**. If the actual payload is malformed, missing a field, or actively malicious (an attacker-controlled API, a compromised third-party SDK, a crafted `postMessage`), the code proceeds as if it received a valid `User` and every downstream consumer inherits that false confidence.
      
      This is not a hypothetical edge case — it is the default behavior of `JSON.parse`, which is typed to return `any` specifically because the compiler cannot know the shape of arbitrary parsed JSON. Any annotation applied after that point (`JSON.parse(raw) as Shape`, or assigning the `any` result to a `Shape`-typed variable) is the developer asserting a guarantee the compiler cannot verify and runtime does not enforce.
      
      ## Officially grounded shape
      
      - `JSON.parse` returns `any`. Assigning that `any` to a typed variable, or asserting it with `as`, produces no runtime check — `typescript-eslint`'s `no-unsafe-assignment`, `no-unsafe-member-access`, and `no-unsafe-return` rules specifically exist to flag this exact pattern (an `any` from `JSON.parse` flowing into a typed context) because it is common enough to warrant dedicated tooling.
      - `no-explicit-any` and the `no-unsafe-*` family are part of `recommended-type-checked` / `strict-type-checked` shared configs — if the diff introduces this pattern in a repo that has those configs active, it should already surface as a lint failure; if it does not surface, either the config isn't active on that file or the pattern is disguised (e.g. via an intermediate `unknown` cast chained through `as`).
      - A type assertion (`as T`) does not perform a structural check; it only suppresses the compiler's own inference. `T!` (non-null assertion) similarly performs no runtime check — it only silences the compiler's null/undefined narrowing.
      - The compiler-sound way to narrow an `unknown` or `any` value to a specific type is a **type guard function** (`function isUser(x: unknown): x is User`) that performs actual runtime property/shape checks, or a schema-validation library (e.g. Zod, io-ts, ArkType — verify current API via `query-docs`/official docs for whichever the repo already uses; do not introduce a new one without the user's decision) whose `.parse()`/`.safeParse()` throws or returns a discriminated result on shape mismatch.
      
      ## Non-negotiable design rules
      
      1. **Every trust boundary needs a paired runtime check, not just a type annotation.** Trust boundaries include: HTTP/fetch response bodies, WebSocket messages, `postMessage` payloads, URL/query-string parsing, `localStorage`/`sessionStorage` reads, environment variables consumed at runtime, file uploads, and any third-party SDK callback whose payload originates outside this codebase's own type-checked code.
      2. **A cast is not a check.** `as T`, `<T>value`, and `!` all compile away. None of them are acceptable as the sole guarantee for external data. Flag them at trust boundaries even if the code "usually works" — the point is what happens on the malformed/adversarial input, not the happy path.
      3. **`unknown` is the correct type for genuinely-unvalidated external input**, not `any`. `unknown` forces every consumer to narrow before use; `any` opts the value (and everything derived from it) out of type checking entirely, often silently propagating far beyond the original ingestion point.
      4. **Validation must match the type it claims to back.** A partial validator (checks `id` and `name` exist but the type declares five required fields) is a false sense of security — flag a type/validator shape mismatch as a defect, not just validator absence.
      5. **Distinguish internal-boundary types from trust-boundary types.** Not every `any`/assertion in a codebase needs a schema validator — internal function calls between already-typechecked code do not cross a trust boundary. Scope the audit to data that originates outside this codebase's own compiled/typechecked surface.
      
      ## Minimal safe audit flow
      
      1. Identify every point in the diff where external data enters: fetch/axios/SDK response handling, `JSON.parse` calls, `postMessage` listeners, `URLSearchParams`/`location.search` reads, `process.env`/`import.meta.env` reads used in logic (not just build-time constants), form-field extraction.
      2. For each, trace whether the value passes through a runtime validator (schema library, hand-written type guard with actual property checks) before being treated as its declared type, or whether a bare assertion/cast/generic-only-annotation is the only "check."
      3. Where a validator exists, confirm its declared shape matches the TypeScript type it's meant to back — a validator checking fewer fields than the type declares is a gap, not full coverage.
      4. Where no validator exists, name the concrete blast radius: what downstream code trusts this value's shape, and what happens if a field is missing/wrong-typed/malicious (e.g. `undefined.toUpperCase()` throwing, or a numeric field actually being an attacker-supplied string flowing into a template literal or SQL-adjacent context).
      
      ## Adversarial checklist
      
      Before approving a trust-boundary type as sound, answer:
      
      - What is the actual runtime function/library call that validates this data, by name — not "it's typed as `User`"?
      - What happens if a required field is absent? Does the validator reject it, or does the type only claim it's required while runtime silently proceeds with `undefined`?
      - What happens if a field has the wrong primitive type (a string where a number is declared)? Coercion, rejection, or silent pass-through?
      - Is the validator's schema kept in sync with the TypeScript type by construction (e.g. `z.infer<typeof schema>` generates the type from the validator) or are they two independently hand-maintained declarations that can drift?
      - If this is a third-party SDK response, does the SDK's own published types (if any) constitute a runtime guarantee, or are they — like all TypeScript types — compile-time only and equally unenforced by the SDK itself?
      
      If these cannot be answered concretely, the trust-boundary claim is unverified — report it as a finding, not a pass.
      
      ## High-risk assumptions to kill
      
      - "It's typed, so it's validated" — types are erased; only executed code validates.
      - "The SDK's TypeScript types mean the SDK enforces the shape" — a third-party SDK's `.d.ts` file is exactly as unenforced at runtime as first-party code's types.
      - "We control the API, so the response is always well-formed" — deploy skew (client ahead of/behind the API), a compromised or misconfigured upstream, or a partial outage returning an error body with a 200 status all violate this assumption in practice.
      - "`as unknown as T` is safer than `as T`" — it silences the double-assertion protection the compiler otherwise gives when the two types are structurally too different to be a plausible mistake; it typically appears specifically to bypass that protection, which is a stronger signal something is being forced through.
      
      ## Verification targets
      
      - `git diff` or `Grep` for `JSON.parse(`, ` as `, and `!` immediately following a value derived from `fetch`, `axios`, `postMessage`, `URLSearchParams`, `process.env`, or a named SDK client.
      - Presence and shape of a schema-validation library call (`.parse(`, `.safeParse(`, or a hand-written `is`-suffixed type-guard function) between the raw external value and its first typed use.
      - If a schema library generates the TypeScript type (e.g. `z.infer<...>`), confirm the type in the diff is actually derived that way rather than hand-declared separately, which would let the two drift.
      
      ## When to push back
      
      Push back if the user asks to:
      
      - add a type annotation to an external-data-handling function as the fix for a bug, without adding a runtime check — the type change alone does not prevent the bug's root cause,
      - suppress a `no-unsafe-assignment`/`no-explicit-any` lint finding at a trust boundary with an inline disable comment instead of adding validation,
      - treat "the SDK is typed" as sufficient justification to skip validating that SDK's response shape.
      
  • metadata.json 1.7 KB
    {
      "id": "typescript-contracts-review",
      "name": "TypeScript Contracts Review",
      "type": "skill",
      "provider": "frontend",
      "harnesses": [
        "claude-code",
        "cursor",
        "codex",
        "gemini",
        "kiro",
        "other"
      ],
      "summary": "Reviews tsconfig strictness posture, any/assertion usage at trust boundaries, and exported public-API type surfaces so type signatures are enforced runtime-shape guarantees rather than decorative annotations, requiring paired runtime validation wherever external data enters the type system.",
      "source_type": "original",
      "official_docs": [
        "https://www.typescriptlang.org/tsconfig",
        "https://www.typescriptlang.org/docs/handbook/2/basic-types.html",
        "https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html",
        "https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html",
        "https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html",
        "https://typescript-eslint.io/rules/"
      ],
      "security_notes": "Flag any, as, and non-null assertions (!) at trust boundaries (parsed JSON, third-party SDK responses, postMessage payloads, URL/query-param parsing) without paired runtime validation — a type annotation is erased at compile time and enforces nothing against a malformed or malicious payload. Flag @ts-ignore/@ts-expect-error without an adjacent justification comment, especially on security-relevant code. Flag any proposal to loosen tsconfig strictness (removing strict, disabling strictNullChecks) without an explicit, separately-reviewed migration plan.",
      "last_verified": "2026-07-02",
      "path": "skills/frontend/typescript-contracts-review",
      "author": "github: VincentChuWaiChow",
      "version": "0.1.0"
    }
    
  • SKILL.md 8.1 KB
    ---
    name: typescript-contracts-review
    description: Review TypeScript diffs and tsconfig strictness posture for sound type contracts — auditing any/assertion usage at trust boundaries, unsound narrowing, and exported public-API type-surface breakage — so that a passing compile is meaningful evidence rather than a decorative pass, and requiring paired runtime validation wherever external data enters the type system.
    allowed-tools: Read Grep Glob Bash(git diff:*) Bash(tsc --noEmit:*) WebFetch
    metadata:
      author: "github: VincentChuWaiChow"
      version: "0.1.0"
      updated: "2026-07-02"
      category: compliance
    ---
    
    # TypeScript Contracts Review
    
    ## Purpose
    
    "The build passes" is only meaningful evidence of type safety if the active tsconfig is actually strict and the code doesn't defeat it with `any`, unchecked assertions, or broad suppression comments — and TypeScript types are fully erased at compile time, so a type annotation on external data (a parsed JSON response, a third-party SDK payload, a URL parameter) provides zero runtime protection unless paired with an actual runtime validator. This skill exists so those two failure modes — a loose or silently-weakened tsconfig, and type annotations that assert safety no runtime check backs up — get audited explicitly instead of being assumed away by a green checkmark. It also checks exported public-API type surfaces so a "just an internal refactor" diff doesn't silently break every downstream consumer of a published package.
    
    ## When to use
    
    Use this skill when the user asks to:
    
    - review a TypeScript/TSX diff for type-safety soundness before merge,
    - audit a codebase's tsconfig strictness posture against current TypeScript-recommended defaults,
    - check `any`/type-assertion/non-null-assertion usage, especially at external-data boundaries,
    - verify a discriminated union or type-guard function actually narrows correctly,
    - assess whether a change to an exported/published package breaks its public API type surface.
    
    Do not use this skill for:
    
    - pure runtime/logic bug hunting that has nothing to do with type contracts — that is a general code-review task,
    - JavaScript files with no type annotations or JSDoc types — there is no type contract to audit,
    - live `tsc --noEmit`/type-coverage execution results interpretation beyond what this static review can determine from the diff — report that a live run is needed rather than fabricating its result.
    
    ## Context7 Documentation Protocol
    
    - Resolve the TypeScript library ID with `resolve-library-id` before ruling on any compiler-flag or strict-family question; do not answer from memory, because recommended defaults change across releases (confirmed via Context7: TypeScript 5.9's `tsc --init` now emits `noUncheckedIndexedAccess: true` and `exactOptionalPropertyTypes: true` as a separate "Stricter Typechecking Options" block alongside `strict: true`, where earlier `tsc --init` output only set `strict: true`).
    - Before asserting which individual flags `strict: true` bundles (`strictNullChecks`, `noImplicitAny`, `noImplicitThis`, `alwaysStrict`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `useUnknownInCatchVariables`), call `query-docs` against the current TypeScript docs rather than reciting a memorized list — the bundle is described as open-ended ("future versions of TypeScript may introduce additional stricter checking under this flag"), so a stale list under-reports what a repo's `strict: true` actually enables on its installed compiler version.
    - Before flagging or endorsing a `typescript-eslint` rule (e.g. `no-explicit-any`, `no-unsafe-assignment`, `no-non-null-assertion`), verify via `query-docs` whether that rule is in the repo's active shared config (`recommended`, `recommended-type-checked`, `strict-type-checked`) — rule membership across those tiers has changed between major versions (confirmed via Context7: the v7→v8 `recommended-type-checked` diff added/removed multiple rules), so do not assume a rule is enabled just because the repo extends a config by name without checking the installed version.
    - Read `package.json`/lockfile first to confirm the installed `typescript` and `typescript-eslint`/`@typescript-eslint/*` major versions before citing version-gated behavior (e.g. `exactOptionalPropertyTypes` semantics, `satisfies` operator availability, `const` type parameters) — do not assume a feature is available because it appears in current docs if the installed major predates it.
    - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label every version-sensitive claim `documentation-based, unverified against installed compiler version`.
    
    ## Lean operating rules
    
    - Never accept "the build passes" as sufficient evidence of type safety without checking the actual tsconfig strict-family flags in effect — a loose config proves far less than developers assume, and `strict: true` alone (pre-5.9 `tsc --init` default) does not imply `noUncheckedIndexedAccess` or `exactOptionalPropertyTypes` are on.
    - Every new `any` must carry an adjacent justification comment; flag unjustified `any` as blocking, especially inside application logic that later consumers trust as validated.
    - Every trust-boundary type (parsed JSON, third-party SDK response, `postMessage` payload, URL/query-param, form input, environment variable) must be paired with an actual runtime validator — a type annotation alone is erased at compile time and enforces nothing at runtime.
    - Treat any proposal to loosen existing tsconfig strictness (removing `strict`, disabling `strictNullChecks`) as requiring an explicit, separately-reviewed migration plan, not a routine PR change.
    - Flag broad `@ts-nocheck`/file-level suppression as blocking by default; require `@ts-ignore`/`@ts-expect-error` to carry an adjacent comment explaining why, weighted higher severity if the suppressed code is security- or trust-boundary-relevant.
    - Do not let generic-type complexity become unreadable; a type requiring a comment to explain what it constrains is a design smell worth simplifying, not a badge of sophistication.
    - Do not run or assert `tsc --noEmit` success from memory — invoke it or explicitly flag that CI must, rather than fabricating a compile-success claim.
    - When a diff touches an exported/published package's public surface, check for a breaking type change (removed export, narrowed parameter type, widened return type becoming a narrower consumer-facing type, added required property) before approving; a type-only change can still be a semver-breaking change even with zero runtime behavior difference.
    
    ## References
    
    Load these only when needed:
    
    - [Strict-flag posture reference](references/strict-flag-posture.md) — use when auditing or recommending a tsconfig strict-family flag set, including which flags are bundled under `strict` vs. separately opt-in (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`), and how to read a repo's actual effective config (including `extends` chains).
    - [Trust-boundary validation patterns](references/trust-boundary-validation.md) — use when auditing external-data ingestion points (API responses, `JSON.parse`, `postMessage`, URL parsing, form/env input) for paired runtime validation against their declared types.
    - [Public API surface diffing](references/public-api-surface-diff.md) — use when a diff touches an exported/published package and a breaking-change check against the previous public `.d.ts`/export surface is needed.
    
    ## Response minimum
    
    Return, at minimum:
    
    - the tsconfig strictness posture summary (which strict-family flags are on/off vs. current TypeScript-recommended defaults, and the compiler version that posture was checked against),
    - the `any`/assertion audit for every new `any`, `as`, and `!` in the diff, each flagged with its justification or lack thereof and file:line evidence,
    - the trust-boundary validation audit for every external-data ingestion point touched, naming the missing or present runtime validator,
    - the public-API surface diff and any breaking-change flags, when the diff touches an exported package,
    - verdict (approve / approve-with-notes / block),
    - residual risk notes for anything requiring a live `tsc --noEmit`/type-coverage run beyond this static diff review.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related