TypeScript Reading Type-Checking Errors
When TypeScript disagrees with your code, it doesn’t crash — it prints a type-checking error: a diagnostic message explaining why the shapes, values, or operations you wrote don’t line up with the types you declared (or that TypeScript inferred). These errors show up in your editor as red squiggles and on the command line when you run tsc. Learning to read them quickly — instead of skimming past the message and guessing — is one of the highest-leverage skills for working productively in TypeScript.
This lesson breaks down the anatomy of a type-checking error, walks through several realistic examples, and shows the mistakes that make errors seem more confusing than they actually are.
Overview: What a Type-Checking Error Actually Is
A type-checking error is produced during TypeScript’s type-checking phase, which happens before (or alongside) compilation to JavaScript. The compiler builds an internal model of every variable, function, and expression’s type, then checks that every assignment, function call, and property access is consistent with that model. If it finds an inconsistency, it reports a diagnostic and — by default — still refuses to consider the code fully correct, though depending on your configuration it may or may not stop JavaScript from being emitted.
You’ll encounter these errors in two equivalent places:
- The command line, by running
tscortsc --noEmit(type-check without producing output files). - Your editor, via the TypeScript language service, which runs the same checker in the background and underlines problems as you type.
Both surfaces report the exact same diagnostics, using the exact same error codes and wording — the editor just shows them earlier, before you’ve even saved the file. It’s important to understand that all of this happens at compile time only. Once your code is compiled to JavaScript, every type annotation is stripped out (a process called type erasure). The JavaScript that runs in Node or the browser has no idea an interface or a generic parameter ever existed — so if a type error genuinely slips through (or you use any to silence one), the *runtime* behavior can still fail in ways the type checker can no longer help you with.
Syntax: Anatomy of an Error Message
A typical command-line error from tsc looks like this:
src/app.ts:12:3 - error TS2322: Type 'string' is not assignable to type 'number'.
12 age = "thirty";
~~~
Found 1 error.
Every part carries information:
| Part | Meaning |
|---|---|
src/app.ts:12:3 |
File path, line, and column where the problem starts. |
error |
Severity — TypeScript also emits warning in some tool integrations, but tsc itself only reports errors. |
TS2322 |
A stable numeric error code. Every distinct kind of mistake has its own code, which you can search online for more detail. |
Type '...' is not assignable to type '...'. |
The human-readable message. Read this as “the source type (left) cannot be used where the target type (right) is expected.” |
The ~~~ underline |
Points at exactly which token triggered the error. |
In an editor, the code and location are implicit (it’s right there under your cursor); hovering shows the same TSxxxx: message text. Some errors span multiple indented lines — that’s a chain, where each line explains one layer of *why* the outer assignment failed. You generally want to read a chain from the bottom up: the deepest, most specific line is the root cause, and the outer lines are just TypeScript explaining how that root cause bubbles up.
Examples
Example 1: A Simple Assignability Error
let age: number = 30;
age = "thirty";
Output (from tsc):
error TS2322: Type 'string' is not assignable to type 'number'.
This is the simplest and most common shape of error. age was declared with the type number, so any later assignment must also be a number. The message names the source type first ('string', what you tried to assign) and the target type second ('number', what was declared) — reading it aloud as “string is not assignable to number” tells you exactly which side to fix. The fix is either to assign a compatible value, or to change the declared type if string was actually intended:
let age: number = 30;
age = 31;
console.log(`Age is now ${age}`);
Output:
Age is now 31
Example 2: A Structural Typing (Excess Property) Error
interface User {
id: number;
name: string;
}
function printUser(user: User): void {
console.log(`${user.id}: ${user.name}`);
}
printUser({ id: 1, name: "Ana", age: 30 });
Output (from tsc):
error TS2345: Argument of type '{ id: number; name: string; age: number; }' is not assignable to parameter of type 'User'.
Object literal may only specify known properties, and 'age' does not exist in type 'User'.
TypeScript uses structural typing: a value is compatible with a type if it has at least the required shape, regardless of name. Normally an object with *extra* properties is still fine to pass around — but when you write an object literal directly at a call site, TypeScript performs a stricter excess property check to catch typos, because there’s no other reference to that object anywhere else that could need the extra field. The second, indented line is the real cause: it names the exact offending property, age. Fix it by removing the excess property (or, if it’s genuinely meant to exist, adding it to the interface):
interface User {
id: number;
name: string;
}
function printUser(user: User): void {
console.log(`${user.id}: ${user.name}`);
}
printUser({ id: 1, name: "Ana" });
Output:
1: Ana
Example 3: A Union Type Error With Per-Member Detail
function formatId(id: string | number): string {
return id.toUpperCase();
}
Output (from tsc):
error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
When a value has a union type, an operation is only allowed if it’s valid for every member of the union. Here toUpperCase exists on string but not on number, so TypeScript rejects the whole call and, helpfully, tells you which specific member of the union broke it. This is TypeScript nudging you toward narrowing — using a runtime check like typeof so that inside each branch, the type is reduced to something the operation is actually valid for:
function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase();
}
return id.toString();
}
console.log(formatId("abc"));
console.log(formatId(42));
Output:
ABC
42
How It Works Step by Step (Under the Hood)
When tsc checks a file, it roughly does the following for every expression:
- Infers or reads the declared type of each side of an operation (assignment, call, property access) — from explicit annotations, contextual typing, or inference from a literal/return value.
- Compares types structurally: for object types, it checks that the source has at least all the required members of the target with compatible types; for unions, it checks the operation against every member; for functions, it checks parameter and return type compatibility.
- Builds a diagnostic chain if the mismatch is nested — e.g. “this function isn’t assignable, because its return type isn’t assignable, because…” — with the deepest cause listed innermost.
- Reports the diagnostic tied to a source location and a stable
TSxxxxcode, without altering how the file would compile to JavaScript (type annotations are simply erased).
Because type information is erased at emit time, a .ts file with type errors can often still produce a .js file (unless noEmitOnError is enabled) — the type checker and the JavaScript emitter are separate passes. This is exactly why reading and fixing these diagnostics matters: they are your only safety net, since none of this checking exists once the code is running.
Common Mistakes
Mistake 1: Reading a Chained Error Top-Down Instead of Bottom-Up
type Handler = (event: string) => void;
const handler: Handler = (event: string): number => {
return event.length;
};
What tsc reports:
error TS2322: Type '(event: string) => number' is not assignable to type 'Handler'.
Type 'number' is not assignable to type 'void'.
Beginners often stop at the first line, conclude “my function type is wrong,” and start rewriting the whole signature. But the outer line is only a summary — the actual problem is on the indented line beneath it: the return type number isn’t assignable to the expected void. Once you read to the bottom, the fix is obvious — either stop returning a value, or change the declared Handler type if a return value is genuinely wanted:
type Handler = (event: string) => void;
const handler: Handler = (event: string): void => {
console.log(event.length);
};
handler("hello");
Output:
5
Mistake 2: Missing the “Did You Mean” Hint
interface Product {
id: number;
price: number;
}
const products: Product[] = [
{ id: 1, pricee: 25 },
{ id: 2, price: 40 },
];
const total = products.reduce((sum, p) => sum + p.price, 0);
console.log(total);
What tsc reports:
error TS2561: Object literal may only specify known properties, but 'pricee' does not exist in type 'Product'. Did you mean to write 'price'?
This is a plain typo, and TypeScript is explicit about it — it even suggests the correction. The mistake here isn’t in the code, it’s in how the error gets read: skimming past the last sentence of a long message and missing a hint that hands you the fix directly. Always read a diagnostic to its final sentence before deciding how to fix it:
interface Product {
id: number;
price: number;
}
const products: Product[] = [
{ id: 1, price: 25 },
{ id: 2, price: 40 },
];
const total = products.reduce((sum, p) => sum + p.price, 0);
console.log(total);
Output:
65
Best Practices
- Run
tsc --noEmitlocally (or rely on your editor) so you see errors before shipping — don’t wait for a build pipeline to catch them. - Fix errors top to bottom, one at a time, then re-check. A single wrong type (especially on a shared interface) can cascade into dozens of downstream errors that disappear once the root cause is fixed.
- In a chained message, read to the innermost/last indented line first — that’s almost always the real cause.
- Note the
TSxxxxcode and search for it if the wording is unclear; the same code always means the same category of problem. - Watch for “Did you mean…” suggestions — TypeScript often already knows the fix.
- Don’t silence an error with
as anyor a non-null assertion (!) just to make it go away — that deletes the safety net instead of fixing the mismatch, and the bug usually resurfaces at runtime instead. - When a message is long, isolate a minimal reproduction (copy just the failing lines into a scratch file) — it’s much easier to read a two-line error in isolation than buried in a large file.
- Remember types don’t exist at runtime — an error tsc lets through (or one you’ve suppressed) becomes an ordinary JavaScript bug, not a type error, once compiled.
Practice Exercises
- Exercise 1: The following declares
let total: number = 0;and later runstotal = total + "5";. Predict the exactTSxxxxcode and messagetscwould report, then rewrite the line so it type-checks and produces5when logged. - Exercise 2: Write an interface
Pointwithx: numberandy: number, then write a functiondistance(a: Point, b: Point): number. Call it once with a correct pair of points, and once with an object literal that’s missing theyproperty, and write out the exact error message TypeScript would give for the second call. - Exercise 3: Given
function describe(value: string | string[]): number { return value.length; }, this actually compiles fine — explain in your own words *why* no union error is reported here, unlike theformatIdexample above (hint: check what membersstringandstring[]have in common).
Summary
- A type-checking error is a compile-time diagnostic from
tsc(or your editor’s language service), not a runtime exception — types are fully erased by the time JavaScript runs. - Every error has a location, a stable
TSxxxxcode, and a message that names the source type and the target type it failed to satisfy. - Multi-line “chained” errors should be read from the innermost line up — that’s where the real cause lives.
- Structural typing means excess-property checks, union-member checks, and function assignability all reason about shape, not names.
- Read a message fully before reacting — TypeScript frequently includes a “Did you mean…” suggestion or a precise sub-cause that hands you the fix.
- Never suppress an error with
anyor!just to stop seeing it; fix the underlying type mismatch instead.
