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.
by @mcollina