systematically debug typescript type errors. find the root expression that produces the wrong type, walk the inference chain, isolate with minimal repro, fix with the most-specific type or assertion. distilled from the top typescript debugging skills in the cross-vendor index. trigger when the user pastes a TS type error or asks why TypeScript thinks X is Y.
--- description: systematically debug typescript type errors. find the root expression that produces the wrong type, walk the inference chain, isolate with minimal repro, fix with the most-specific type or assertion. trigger when the user pastes a TS type error or asks why TypeScript thinks X is Y. --- # debug typescript types a procedural way to debug typescript type errors that does not devolve into adding `as any`. follow the inference chain to the root, isolate, then fix at the root with the most specific type that satisfies both the constraint and the actual runtime value. ## intent most type errors are not where they appear, they are upstream. the error at line 47 says "string is not assignable to number", but the actual problem is a generic that got inferred too wide three layers up. the goal is to find the upstream source, fix it once, and let the rest of the inference resolve. ## inputs - the full error message (typescript's errors are noisy but every clause matters) - the file plus line where it surfaced - whether the code is yours, a dependency's types, or a generated declaration ## procedure ### step 1, read the error literally typescript errors are precise. read every clause, not just the first sentence: - "type 'X' is not assignable to type 'Y'": the rhs type does not satisfy the lhs constraint - "property 'foo' does not exist on type 'X'": the type narrowing dropped 'foo' somewhere - "type 'X | Y' is not assignable to type 'X'": something widened a union back open write down the actual reported type vs the actual expected type. do not skim. ### step 2, find the root expression hover the expression in your editor (or use `// $ExpectType`) and walk back upstream. for each hover: - is the type what you expect at this point? - if yes, the problem is downstream. continue downstream. - if no, the problem is here or upstream. continue upstream. stop when you hit the first place where the type went wrong. that is the root. ### step 3, isolate with a minimal repro if the root is not obvious, paste the bad expression and its dependencies into a fresh `.ts` file or the typescript playground. strip everything that does not contribute. if the error reproduces on 10 lines, the cause is in those 10 lines. ### step 4, classify the root cause most type errors fall into one of these buckets: - **wrong generic inference**: a generic got inferred to a wider type than you wanted. fix: pass the type argument explicitly. `fn<MyType>(x)` instead of `fn(x)`. - **lost narrowing**: a control-flow narrowing got dropped (assignment to a closure variable, await re-widening, etc). fix: re-narrow with a type guard, or use a const local. - **misaligned declaration**: a .d.ts says one thing but the runtime returns another. fix: update the types (file an issue or local override) and add a runtime check. - **structural mismatch**: two types look the same but one has an extra readonly, optional, or method signature. fix: make them genuinely compatible, do not assert through. - **any leakage**: somewhere upstream is `any`, polluting downstream. fix: find the `any` source, give it a real type. `unknown` is almost always the right intermediate. ### step 5, fix at the root with the narrowest correct type apply the fix at the root, not at the symptom. preferred order: 1. fix the declaration if it was wrong (most durable) 2. pass an explicit generic argument 3. add a type guard or assertion function 4. use `satisfies` (preserves inference) 5. use `as <SpecificType>` (only if the runtime invariant is real and provable) 6. last resort: `as any` then narrow immediately. never leave `as any` in committed code. ### step 6, verify the original error is gone and no new ones appeared re-run tsc. the original error should be gone. if new errors appeared, the fix moved the problem rather than solving it. go back to step 2 with the new error. ## decision points - **the type comes from a third-party package and the .d.ts is wrong**: short-term, use module augmentation or a local declaration override. long-term, file a PR to definitelytyped or the package. - **the inferred type is technically correct but inconvenient**: do not assert it away. either redesign the api to make the type narrower, or accept the inconvenience and handle the union explicitly. - **the error is in a generated file (graphql codegen, prisma, openapi)**: do not edit the generated file. fix the schema or the codegen config upstream. ## output contract a fix that: - resolves the original error - does not introduce `as any` or `@ts-ignore` unless commented with a real reason and a follow-up - leaves the type at least as narrow as before - passes `tsc --noEmit` cleanly ## outcome signal re-running the build is green. the next person who reads the code can see at the type level what the expression is. if your fix made them say "where is this type coming from", the fix is incomplete. ## notes - typescript errors at line N are almost never about line N. always walk upstream first. - `satisfies` is the modern equivalent of "assert without losing inference". use it instead of `as` when you can. - if you cannot reproduce the error in isolation, the problem is in the build config (tsconfig path mappings, multiple tsconfigs, project references), not the code.
don't have the plugin yet? install it then click "run inline in claude" again.
expanded inputs section with specific build and editor requirements, added explicit input/output pairs to each procedure step, clarified decision logic including tsconfig edge cases and async narrowing loss, strengthened outcome signal with readability criterion, and added practical debugging notes on satisfies and $ExpectType tooling.
a procedural way to debug typescript type errors that does not devolve into adding as any. follow the inference chain to the root, isolate, then fix at the root with the most specific type that satisfies both the constraint and the actual runtime value.
most type errors are not where they appear, they are upstream. the error at line 47 says "string is not assignable to number", but the actual problem is a generic that got inferred too wide three layers up. the goal is to find the upstream source, fix it once, and let the rest of the inference resolve. use this skill when a user pastes a typescript type error, asks why typescript thinks X is Y, or is stuck in a cycle of narrowing assertions instead of fixing the root cause.
typescript errors are precise. read every clause, not just the first sentence.
inputs: the full error message from tsc or your editor.
process:
write down the actual reported type versus the actual expected type. do not skim. every word in the error is a clue.
outputs: a written statement of what the error says, not what you think it means.
hover the expression in your editor (vscode: hold shift and click, or use the hover tooltip) or use the assertion form // $ExpectType <Type> to query typescript directly. walk back upstream from the error site.
inputs: the file, the line where the error surfaced, an editor with typescript language support.
process:
outputs: the line number and expression where the type first went wrong.
if the root is not obvious, paste the bad expression and its dependencies into a fresh .ts file or the typescript playground (typescriptlang.org/play). strip everything that does not contribute to the error.
inputs: the code context around the root expression, tsconfig settings if unusual.
process:
outputs: a typescript playground link or a standalone .ts file that reproduces the error with minimal code.
most type errors fall into one of these buckets.
inputs: the minimal repro, the type that was inferred versus the type that was expected.
process: pick the category that matches.
fn(x) infers T to string | number when you meant string. fix: pass the type argument explicitly, fn<string>(x).if (x instanceof MyClass) { fn(x); } but fn does not see x as MyClass. fix: re-narrow with a type guard, or use a const local scoped to the narrow branch.string but it returns string | null. fix: update the types (file an issue or local override) and add a runtime check.{ x: number; y: number } versus { readonly x: number; y: number } do not match without assertion. fix: make them genuinely compatible, do not assert through the mismatch.any, polluting downstream inference. example: a function parameter is any, so its return type is any, so all assignments from it are any. fix: find the any source, give it a real type. unknown is almost always the right intermediate.outputs: a diagnosis of which category this is, plus a hypothesis of where in the call stack the problem originated.
apply the fix at the root, not at the symptom. prefer these options in order.
inputs: the root expression, the diagnosis from step 4, the tsconfig strict mode setting.
process:
fn(x) to fn<MyType>(x). this prevents typescript from inferring too wide.x is NarrowType and call it in a conditional. this narrows the type in the subsequent block without losing runtime safety.satisfies (preserves inference): const x = expr satisfies MyType checks that expr conforms to MyType without widening x's inferred type. modern typescript, very useful.as <SpecificType> (only if runtime invariant is real and provable): expr as SpecificType asserts the type without a runtime check. only do this if you can prove at runtime that the expression is definitely SpecificType. add a comment explaining the runtime invariant.as any then narrow immediately: never leave as any in committed code. if you must use it, follow it immediately with a narrow, add a TODO comment, and file a follow-up task.outputs: code at the root expression that resolves the inferred type mismatch.
re-run tsc --noEmit or your build.
inputs: the fixed code.
process:
outputs: a clean tsc exit code (0), or a list of any new errors that appeared.
a fix that:
as any or @ts-ignore in production code (or if it does, it is commented with a real reason and a follow-up task is filed)tsc --noEmit --strict cleanlyre-running the build is green. the next person who reads the code can see at the type level what the expression is, and how it got there. if your fix made them say "where is this type coming from" or "why did you use as any here", the fix is incomplete. a comment explaining the non-obvious part of the fix is a green light.
satisfies is the modern equivalent of "assert without losing inference". use it instead of as when you can. it was added in typescript 4.9.// $ExpectType <Type> is a debug tool. hover over an expression, or write // $ExpectType string on the next line, and typescript will error if the type does not match. use it to verify your hypothesis about what the type should be at a given point.any, unknown, or object, the function's return type and all assignments downstream will be too. be explicit.credits: original skill by implexa.