Free Handbook · Runs in your browser

Errors & Debugging

How to read a tsc error, an indexed reference of the fifteen error codes every TypeScript developer hits, the strict flags that produce them, and how to read a wall of nested generic error text.

0 / 105 lessons🔥 0 day streak
ShareXLinkedIn

Module 10 · what you'll be able to do

  • Read a tsc error's file:line:col, error code and squiggle context without guessing
  • Recognise the fifteen most common TypeScript error codes by number and know the fix for each
  • Explain what strictNullChecks, noImplicitAny, strictFunctionTypes and noUncheckedIndexedAccess each catch
  • Parse a large nested-generic error message by reading it inside-out, root cause first
01

Anatomy of a tsc error

Every tsc error has the same four parts, in the same order, whether it is one line or fifty. Learning to parse the shape — not the specific message — is what makes an error in a library you have never opened readable on sight.

text
app.ts:3:1 - error TS2322: Type 'string' is not assignable to type 'number'.

3 age = "thirty"
  ~~~
  1. 1
    file:line:col

    app.ts:3:1 — the file, then the exact line and column the checker was looking at when it decided something was wrong. Your editor jumps straight here.

  2. 2
    The TS code

    TS2322 — a stable, searchable number. The same bug always produces the same code, so this is what you paste into a search engine, not the whole sentence.

  3. 3
    The message

    "Type 'string' is not assignable to type 'number'." — plain English, but read literally: it names the two types in conflict, in the order source-type then target-type.

  4. 4
    Source context

    The numbered line, and a row of ~~~ under exactly the span the checker means. On a long line this is what tells you which sub-expression is the problem, not the whole statement.

SignalWhat it tells you
Position of the ~~~The narrowest expression the checker blames — often not the whole line
Order of types in the messageAlmost always "the thing you have" then "the thing it needed to be"
Multiple stacked errors on one runRead the first one first — later ones are frequently just consequences of it
A message several paragraphs longA nested/generic mismatch — read it bottom-up (last lesson in this module)
CI logs and --pretty
What you see above is tsc's default, colourised, human-friendly format. Plain CI logs sometimes show the same information without the squiggle, as a single line — the file:line:col and TS code are always there either way; that is the part to anchor on.
02

Assignment & argument errors

The four errors you meet earliest, because they fire the moment a value of one type meets a slot that wants another — an assignment, a function call, or a comparison.

Error you will hit

1. TS2322 — Type 'X' is not assignable to type 'Y'

typescript
let age: number
age = "thirty"
app.ts:2:1 - error TS2322: Type 'string' is not assignable to type 'number'.

2 age = "thirty"
  ~~~
Why the compiler said that

The most general assignability error — it fires anywhere a value is placed into a typed slot that cannot hold it: a variable assignment, a return statement, an object literal against an interface, a JSX prop. The message always reads source type first, target type second, so "Type 'string' is not assignable to type 'number'" means the string is what you tried to put in, and number is what the slot was declared as.

The fix

Fix whichever side is wrong: change the value to match the declared type, widen the declared type if it was too narrow, or convert explicitly (Number(age)) if the conversion is intentional and safe.

Error you will hit

2. TS2345 — Argument of type 'X' is not assignable to parameter of type 'Y'

typescript
function total(price: number, quantity: number): number {
  return price * quantity
}
total("12", 3)
app.ts:4:7 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

4 total("12", 3)
        ~~~~
Why the compiler said that

The same idea as TS2322, specifically at a function call site: one of the arguments you passed does not match the type of the parameter it is filling. The column points at the exact argument, which matters most when a call has several arguments and only one is wrong.

The fix

Pass the right type at the call site — usually a missing conversion (Number("12")) or a variable that was the wrong type further up. Do not widen the parameter type just to silence this; that removes the protection the function existed to add.

Error you will hit

3. TS2554 — Expected N arguments, but got M

typescript
function createUser(name: string, role: string): void {}
createUser("Ada")
app.ts:2:1 - error TS2554: Expected 2 arguments, but got 1.

2 createUser("Ada")
  ~~~~~~~~~~~~~~~~~~
Why the compiler said that

A plain arity mismatch — too few or too many arguments for the function's declared parameter list. It is distinct from TS2345 (wrong type) because here the number of arguments itself is wrong, not their types. A common trigger: a parameter you meant to make optional but forgot the ? on.

The fix

Pass the missing argument, or make the parameter optional with role?: string (and handle the undefined case inside the function) if callers legitimately may omit it.

Error you will hit

4. TS2367 — This comparison appears to be unintentional

typescript
type Status = "pending" | "active" | "done"
function isFinished(status: Status) {
  return status === "complete"
}
app.ts:3:10 - error TS2367: This comparison appears to be unintentional because the types 'Status' and '"complete"' have no overlap.

3   return status === "complete"
           ~~~~~~~~~~~~~~~~~~~~~
Why the compiler said that

TypeScript narrowed status to a specific union of string literals, and noticed the value you are comparing it against is not, and can never be, one of them — so the comparison can only ever be false, which is almost certainly not what was intended. This catches typos in string comparisons that a plain JavaScript project would never flag, because JavaScript happily compares any two strings.

The fix

Fix the typo ("done", not "complete"), or if the union itself is missing a valid state, add it to the type definition.

03

Object shape errors

These fire when the checker is comparing not a single value, but the whole shape of an object against an interface or type — a typo in a property name, a required field left out, or a name that simply does not exist anywhere in scope.

Error you will hit

5. TS2339 — Property 'x' does not exist on type 'Y'

typescript
interface User {
  id: number
  name: string
}
function greet(user: User) {
  return `Hi ${user.nmae}`
}
app.ts:6:23 - error TS2339: Property 'nmae' does not exist on type 'User'.

6   return `Hi ${user.nmae}`
                    ~~~~
Why the compiler said that

The single most common TypeScript error in real codebases: you accessed a property the declared type does not have. Nine times out of ten this is a typo (as here — nmae for name); the rest of the time the object genuinely does not have that field, and the type is correctly stopping a bug before it becomes "undefined is not a function" at runtime.

The fix

Fix the spelling, or if the property should exist, add it to the interface. Never reach for as any here — that deletes the checking for the whole expression, not just this property.

Error you will hit

6. TS7006 — Parameter 'x' implicitly has an 'any' type

typescript
function add(a, b) {
  return a + b
}
app.ts:1:14 - error TS7006: Parameter 'a' implicitly has an 'any' type.

1 function add(a, b) {
               ~
Why the compiler said that

With noImplicitAny on (part of strict), a parameter with no annotation and no default value that the checker cannot infer from context becomes an error instead of a silent any. Without this flag, a and b would type-check no matter what was passed — a string, an object, nothing — defeating the entire point of using TypeScript on this function.

The fix

Annotate the parameters: function add(a: number, b: number). If the function genuinely needs to accept anything, write unknown and narrow inside the body — never a bare, unannotated parameter.

Error you will hit

8. TS2739 / TS2740 — Type is missing properties from type

typescript
interface Task {
  id: number
  text: string
  done: boolean
}
const t: Task = { id: 1, text: "ship it" }
app.ts:6:7 - error TS2739: Type '{ id: number; text: string; }' is missing the following properties from type 'Task': done

6 const t: Task = { id: 1, text: "ship it" }
        ~
Why the compiler said that

An object literal assigned directly to a typed slot is checked for completeness: every required property of the target type has to be present. TS2739 lists what is missing when a few properties are absent; TS2740 is the same idea worded for when almost nothing matches at all. This only fires on fresh object literals — a variable of a wider type assigned in requires an explicit cast, which is a different, louder mistake.

The fix

Add the missing property (done: false), or mark it optional on the interface (done?: boolean) if it is genuinely allowed to be absent at construction time.

Error you will hit

13. TS2304 — Cannot find name 'x'

typescript
function formatDate(d: Date) {
  return formtDate(d)
}
app.ts:2:10 - error TS2304: Cannot find name 'formtDate'. Did you mean 'formatDate'?

2   return formtDate(d)
           ~~~~~~~~~
Why the compiler said that

No binding with this exact name is visible in scope — a typo, a missing import, a variable declared inside a different block, or a global that exists at runtime (like a browser API) but has no type declaration in this project. Recent TypeScript versions often suggest the closest real name, as shown here.

The fix

Match the spelling exactly, including case. If it is meant to come from another module, add the import. If it is a real global, check whether the right lib (Module 11) or an @types/ package (Module 09) is missing.

Error you will hit

14. TS2551 — Property 'x' does not exist on type 'Y'. Did you mean 'z'?

typescript
interface User {
  firstName: string
  lastName: string
}
function fullName(u: User) {
  return u.firstname + " " + u.lastName
}
app.ts:5:25 - error TS2551: Property 'firstname' does not exist on type 'User'. Did you mean 'firstName'?

5   return u.firstname + " " + u.lastName
                        ~~~~~~~~~
Why the compiler said that

A close cousin of TS2339 with one difference: the checker found a real property on the type whose name is close enough (usually just a case difference) that it is confident enough to suggest it directly. It is the same root cause — a typo — with a friendlier message.

The fix

Take the suggestion if it is right (u.firstName). If it is not, the real fix is the same as TS2339: correct the spelling or add the field.

04

Null & undefined errors

strictNullChecks (bundled into strict) is responsible for most of these — without it, null and undefined are silently assignable to every type, and TypeScript cannot warn you about any of the three errors below at all.

Error you will hit

5. TS2532 / TS18048 — Object is possibly 'undefined'

typescript
const users = new Map<number, { name: string }>()
function greet(id: number) {
  return `Hi ${users.get(id).name}`
}
app.ts:3:23 - error TS2532: Object is possibly 'undefined'.

3   return `Hi ${users.get(id).name}`
                      ~~~~~~~~~~~~~
Why the compiler said that

Map.get returns V | undefined — there is no guarantee the key exists — and strictNullChecks refuses to let you read a property off a value that might be undefined. This is the type-checker catching the exact bug behind "Cannot read properties of undefined" one step before it can ever happen at runtime. TS18048 is the same idea reported for a plain possibly-undefined variable rather than an expression.

The fix

Check first: const user = users.get(id); if (!user) return "unknown"; return user.name. Or use optional chaining when a fallback is fine: users.get(id)?.name ?? "unknown".

Error you will hit

6. TS2531 — Object is possibly 'null'

typescript
const el = document.getElementById("app")
el.textContent = "Ready"
app.ts:2:1 - error TS2531: Object is possibly 'null'.

2 el.textContent = "Ready"
  ~~
Why the compiler said that

document.getElementById is typed as returning HTMLElement | null, because the id might not exist on the page. This is the same protection as TS2532, specifically for the DOM APIs that return null (rather than undefined) when nothing is found — one of the sharpest edges beginners hit moving from plain JavaScript, where this line would run without complaint until the element genuinely was missing.

The fix

Guard it: if (el) el.textContent = "Ready", or assert you know better only when you truly do: el!.textContent = "Ready" (the non-null assertion — use sparingly, it silences the check rather than satisfying it).

Error you will hit

15. TS18046 — 'x' is of type 'unknown'

typescript
try {
  JSON.parse("{ bad json")
} catch (err) {
  console.log(err.message)
}
app.ts:4:19 - error TS18046: 'err' is of type 'unknown'.

4   console.log(err.message)
                  ~~~
Why the compiler said that

With useUnknownInCatchVariables (on by default under strict since TypeScript 4.4), a caught error is typed as unknown rather than any — correct, because JavaScript allows throw of literally any value, not only Error objects, so the checker cannot assume .message exists. unknown is TypeScript's "could be anything, prove it before you use it" type; it blocks every operation until you narrow it.

The fix

Narrow before use: if (err instanceof Error) console.log(err.message), with an else branch (or a coerced String(err)) for the case something else was thrown.

05

Generic, overload & access-modifier errors

The remaining three come up once code starts using generics, function overloads, and class access modifiers — the parts of the type system that model "this works for a family of types" or "this class controls its own state" rather than one fixed shape.

Error you will hit

10. TS2769 — No overload matches this call

typescript
function pick(items: number[], count: number): number[]
function pick(items: string[], count: number): string[]
function pick(items: unknown[], count: number): unknown[] {
  return items.slice(0, count)
}

pick([1, 2, 3], "2")
app.ts:6:1 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(items: number[], count: number): number[]', gave the following error.
    Argument of type 'string' is not assignable to parameter of type 'number'.
  Overload 2 of 2, '(items: string[], count: number): string[]', gave the following error.
    Argument of type 'number[]' is not assignable to parameter of type 'string[]'.

6 pick([1, 2, 3], "2")
  ~~~~~~~~~~~~~~~~~~~~
Why the compiler said that

A function with multiple declared overload signatures was called with an argument combination that matches none of them. The message lists every overload it tried and why each one failed — read it as several small TS2345 errors stacked together, one per candidate signature, not one error with a confusing extra layer.

The fix

Find the overload you actually meant, and match its argument types exactly — here, either pass a number count (pick([1,2,3], 2)) or an array whose element type matches an existing overload.

Error you will hit

11. TS2341 — Property is private and only accessible within class

typescript
class Account {
  private balance: number = 0
  deposit(n: number) { this.balance += n }
}
const a = new Account()
a.balance
app.ts:6:3 - error TS2341: Property 'balance' is private and only accessible within class 'Account'.

6 a.balance
    ~~~~~~~
Why the compiler said that

private is a compile-time-only guarantee — TypeScript is enforcing that outside code cannot read or write this field, even though nothing stops it at runtime (the field is a normal property on the compiled object). This is intentional encapsulation working exactly as designed, not a bug in the class.

The fix

Add a public method or getter that exposes only what the class wants to (getBalance()), rather than widening the field to public. If you find yourself needing the raw value outside the class often, that is a sign the class's public API is incomplete.

Error you will hit

12. TS2540 — Cannot assign to 'x' because it is a read-only property

typescript
interface Config {
  readonly apiUrl: string
}
function setup(config: Config) {
  config.apiUrl = "https://api.example.com"
}
app.ts:5:10 - error TS2540: Cannot assign to 'apiUrl' because it is a read-only property.

5   config.apiUrl = "https://api.example.com"
           ~~~~~~
Why the compiler said that

readonly allows a property to be read freely but blocks any assignment to it after the object is constructed — a compile-time-only lock, like private, with nothing stopping a plain JavaScript caller. It catches accidental mutation of values meant to be fixed for the life of the object: configuration, IDs, anything set once at creation.

The fix

Construct a new object instead of mutating: const updated = { ...config, apiUrl: "..." }. If mutation genuinely is intended, the field should not have been declared readonly in the first place.

06

Strict mode flags: before and after

strict: true in tsconfig turns on a family of roughly eight flags at once (Module 11 lists the whole family). Four of them are responsible for most of the errors above — seeing each one turned off, then on, is the fastest way to understand what it is actually doing.

typescript
function greet(name: string) {
  return "Hello, " + name.toUpperCase()
}
let user: string = null      // allowed — null fits every type
greet(user)                   // compiles; TypeError at runtime

strictNullChecks: false — compiles, and is wrong

typescript
let user: string = null
// app.ts:1:5 - error TS2322: Type 'null' is not assignable to type 'string'.

strictNullChecks: true — tsc refuses before it ever runs

typescript
function add(a, b) {
  return a + b
}
add("2", 3)     // compiles and returns "23" — no warning at all

noImplicitAny: false — a and b are silently any, unchecked

typescript
function add(a, b) {
  return a + b
}
// app.ts:1:14 - error TS7006: Parameter 'a' implicitly has an 'any' type.

noImplicitAny: true — tsc TS7006, forces you to decide the type

typescript
type ClickHandler = (e: MouseEvent) => void
let onClick: ClickHandler = (e: Event) => {}   // accepts a NARROWER param — unsound

strictFunctionTypes: false — an unsound handler assignment compiles

typescript
type ClickHandler = (e: MouseEvent) => void
let onClick: ClickHandler = (e: Event) => {}
// app.ts:2:5 - error TS2322: Type '(e: Event) => void' is not assignable to type 'ClickHandler'.
//   Types of parameters 'e' and 'e' are incompatible.
//     Type 'MouseEvent' is not assignable to type 'Event'.

strictFunctionTypes: true — tsc catches the narrowing

typescript
const scores: number[] = [10, 20, 30]
const tenth = scores[10]         // type: number — but it is actually undefined
console.log(tenth.toFixed(2))    // compiles; crashes at runtime

noUncheckedIndexedAccess: false (the tsc default) — indexing lies about undefined

typescript
const scores: number[] = [10, 20, 30]
const tenth = scores[10]         // type: number | undefined
console.log(tenth.toFixed(2))
// app.ts:3:13 - error TS18048: 'tenth' is possibly 'undefined'.

noUncheckedIndexedAccess: true — the real type is number | undefined

noUncheckedIndexedAccess is not part of strict
Unlike the other three above, noUncheckedIndexedAccess is not bundled into strict: true — it is opt-in separately, because it is stricter than most existing codebases expect and would flag a large volume of pre-existing array/record indexing. Turn it on deliberately; Module 11 lists exactly which flags strict does and does not include.
07

Reading generic error soup

A generic mismatch produces an error that nests: an outer sentence about the whole call, then indented sentences drilling into the specific mismatched piece, sometimes four or five levels deep. Reading top to bottom feels natural and is exactly backwards — the useful information is at the bottom, and everything above it is the checker explaining how it got there.

VisualizeReading a nested generic error inside-outStep 1 / 5
app.ts:14:3 - error TS2345: Argument of type '(state: State, action: { type: "increment"; payload: number }) => State' is not assignable to parameter of type 'Reducer<State, Action>'.
Types of parameters 'action' are incompatible.
Type 'Action' is not assignable to type '{ type: "increment"; payload: number; }'.
Property 'payload' is missing in type '{ type: "reset"; }' but required in type '{ type: "increment"; payload: number; }'.
Line 1

Start by skimming, not reading — this top line only tells you WHERE (line 14, a function argument) and WHAT KIND of error (TS2345, an argument mismatch). Do not try to parse the full type names yet; they are usually too long to hold in your head, and you rarely need to.

Variables now

nothing yet

All 5 steps as a table
StepLineWhat happenedVariables now
11Start by skimming, not reading — this top line only tells you WHERE (line 14, a function argument) and WHAT KIND of error (TS2345, an argument mismatch). Do not try to parse the full type names yet; they are usually too long to hold in your head, and you rarely need to.
22Each indented line narrows the previous one by one layer. "Types of parameters 'action' are incompatible" tells you the mismatch is not the whole function, just its second parameter — ignore everything about the return type or the first parameter.
33Narrower still: the checker is now comparing one specific type inside Action against one specific literal object type. Still an overview, not the cause.
44This is the line that matters, and it is always the LAST one: "Property 'payload' is missing in type '{ type: \"reset\" }'". The reducer you wrote assumed every action has a payload, but the Action union also allows a 'reset' action that has none — a real bug, not a false positive.
54Read a nested error bottom-up: the last line is the root cause, in concrete terms with no generic names; every line above it just explains how the checker traced the mismatch down to that point. Fix the innermost problem (make payload optional, or handle 'reset' before accessing payload) and the whole stack of outer errors disappears at once.
One fix often clears many errors
Because every outer line in a nested error is a consequence of the innermost one, fixing the bottom line and recompiling frequently makes several "different" errors vanish together. If an editor shows five red squiggles in one area of generic code, look for the one root cause before fixing them individually.
SeniorYou get a twelve-line nested generic type error from a library you did not write. How do you approach it?

Skip the outer lines on the first pass — they describe how the checker traced the mismatch, not the mismatch itself — and jump straight to the innermost, most concrete line, which is usually a plain missing-property or wrong-literal-type statement with real names instead of generic parameters. Reconstruct the cause from there, then work back up only if the fix is not obvious, since each outer line is context for why the checker even compared those two types. If the library's own types are at fault rather than the calling code, that is the point to consider a narrower local type, a type assertion at the boundary, or reporting it upstream — not disabling checking for the whole call.

What they are really testing: Whether they default to reading top-down and giving up, or know the inside-out approach.

Finish the TypeScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.