How to Fix Common TypeScript Type Errors: Practical Guide With Examples
On this page
How to Fix Common TypeScript Type Errors: Practical Guide With Examples
I genuinely love TypeScript's type system — it's saved me from countless runtime bugs and made refactoring way less terrifying. But I also have a deeply personal relationship with those cryptic red squiggly lines. Some days TypeScript feels like a helpful guardrail; other days it feels like it's actively fighting me over something that clearly works.
After years of writing TypeScript daily, I've seen the same errors over and over. Here's my field guide to the most common ones and how to actually fix them.
Reading TypeScript Error Messages
Before we dive in: TypeScript's error messages look intimidating but they follow a pattern. Look for "Type 'X' is not assignable to type 'Y'" — that's TypeScript telling you what it found versus what it expected.
Pro tip: when you see a long, deeply nested error, read from the bottom up. The deepest line usually points to the exact property causing the problem. I wasted months reading these top-down before someone told me this.
Type 'X' Is Not Assignable to Type 'Y' (TS2322)
The all-time champion of TypeScript errors. You're assigning a value of one type to something that expects a different type.
// Error: Type 'string' is not assignable to type 'number'
let age: number = "twenty-five";
Fix: Make the value match the declared type:
let age: number = 25;
// OR, if a string is intentional:
let age: string = "twenty-five";
This also pops up with objects when you're missing a property:
interface User {
name: string;
email: string;
}
// Error: Property 'email' is missing in type '{ name: string; }'
const user: User = { name: "Alice" };
Fix: Supply all required properties, or mark optional ones with ?:
interface User {
name: string;
email?: string; // now optional
}
Property Does Not Exist on Type (TS2339)
You're accessing a property TypeScript doesn't know about.
const response = { status: 200, data: "OK" };
console.log(response.statusCode); // Error: Property 'statusCode' does not exist
Fix: Check for typos first — that's the cause 90% of the time. If the property genuinely exists at runtime:
// Option 1: Fix the typo
console.log(response.status);
// Option 2: Index signature for dynamic shapes
const response: { [key: string]: unknown } = fetchData();
console.log(response.statusCode);
// Option 3: Extend the type
interface ApiResponse {
status: number;
data: string;
statusCode?: number;
}
Object Is Possibly 'undefined' or 'null' (TS2532 / TS18048)
With strictNullChecks enabled (and you should have it on), TypeScript forces you to handle the possibility that something might not exist.
function getUser(id: number): User | undefined {
return users.find(u => u.id === id);
}
const user = getUser(1);
console.log(user.name); // Error: Object is possibly 'undefined'
Fix: Narrow the type before using it:
// Option 1: Guard clause (my preference)
const user = getUser(1);
if (!user) {
throw new Error("User not found");
}
console.log(user.name); // Safe — TypeScript knows user is defined
// Option 2: Optional chaining
console.log(user?.name);
// Option 3: Non-null assertion (use sparingly)
console.log(user!.name);
That ! operator is tempting but dangerous. Every ! is you promising TypeScript "trust me, this won't be null." If you're wrong, you get exactly the runtime crash TypeScript was trying to prevent. I reserve it for genuinely known-safe situations, like immediately after an assertion function.
Argument of Type 'X' Is Not Assignable to Parameter of Type 'Y' (TS2345)
You're passing the wrong type to a function:
function greet(name: string) {
return `Hello, ${name}`;
}
greet(42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Fix: Convert the value or update the function signature:
greet(String(42)); // convert the argument
greet(42 as any); // escape hatch (avoid in production)
Here's a subtler version that catches people off guard:
function createUser(config: { name: string; role: "admin" | "user" }) {}
const config = { name: "Bob", role: "admin" };
createUser(config); // Error: string is not assignable to '"admin" | "user"'
TypeScript widens config.role to string because it's a mutable variable. Fix with as const:
const config = { name: "Bob", role: "admin" } as const;
// OR
const config: { name: string; role: "admin" | "user" } = { name: "Bob", role: "admin" };
I hit this one constantly when passing config objects to functions. The as const trick is one of the most useful things I've learned in TypeScript.
Cannot Find Module or Its Type Declarations (TS2307)
TypeScript can't locate the module you're importing, or it found the code but there are no type definitions.
import something from "some-library"; // Error: Cannot find module 'some-library'
Fix:
- Install the package if missing:
npm install some-library - Install types:
npm install -D @types/some-library - Create a declaration file if no types exist anywhere. Add
declarations.d.ts:
declare module "some-library" {
const value: any;
export default value;
}
- Check your tsconfig.json
pathsandmoduleResolutionsettings if it's a local module.
Type Assertions and When They're Actually Okay
Type assertions (as) tell the compiler to treat a value as a specific type:
const input = document.getElementById("username") as HTMLInputElement;
console.log(input.value); // Works — TypeScript treats it as an input element
When to use them:
- DOM element access (you know the element type better than TypeScript does)
- Working with validated API responses
- Gradual migration from JavaScript
When NOT to use them:
- To silence errors you don't understand
- As a substitute for proper type narrowing
- Sprinkling
as anythroughout production code (I've inherited codebases like this — it's painful)
Union Types and Type Narrowing
Unions are powerful but you need to narrow them before using type-specific features:
function format(value: string | number) {
return value.toFixed(2); // Error: 'toFixed' does not exist on type 'string'
}
Fix with narrowing:
function format(value: string | number) {
if (typeof value === "number") {
return value.toFixed(2);
}
return value.toUpperCase(); // TypeScript knows it's a string here
}
For custom objects, discriminated unions are your friend:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
}
}
Generic Type Confusion
Generics produce confusing errors, especially when you're still getting comfortable with them:
function first<T>(arr: T[]): T {
return arr[0]; // Could be undefined if array is empty
}
A common mistake is over-constraining:
// Too restrictive
function merge<T extends string>(a: T, b: T): string {
return a + b;
}
// Better
function merge<T extends string | number>(a: T, b: T) {
return `${a}${b}`;
}
When generic errors get confusing, try explicitly specifying the type at the call site:
const result = myFunction<string>("hello"); // makes expected types clear
This often reveals exactly where the mismatch is.
Excess Property Checks
TypeScript catches extra properties on direct object literal assignments:
interface Options {
color: string;
width: number;
}
// Error: 'height' does not exist in type 'Options'
const opts: Options = { color: "red", width: 100, height: 50 };
Interestingly, this only applies to direct literal assignment. Via a variable, it's fine:
const raw = { color: "red", width: 100, height: 50 };
const opts: Options = raw; // No error
Fix: Add the property to the interface, remove it from the object, or use an index signature if dynamic properties are expected.
Debugging Tips That Actually Help
- Hover over variables in your editor. The inferred type tooltip is often more useful than the error message.
- Simplify the type. If a complex generic is causing issues, temporarily replace it with a concrete type to isolate the problem.
- Use
satisfiesto check a value conforms to a type without altering inference. - Read errors bottom-up. Nested errors show the root cause at the end.
- Avoid
anyas a fix. Useunknownand narrow from there. - Keep
strict: truein tsconfig. It catches more upfront and prevents bad habits.
FAQ
Q: Should I use any to quickly fix type errors?
A: Please don't. any disables type checking entirely for that value. Use unknown and narrow with runtime checks. Save any for genuine escape hatches during JS migration or truly untyped boundaries.
Q: What's the difference between type and interface?
A: interface supports declaration merging and is preferred for object shapes. type is more versatile — unions, intersections, primitives, mapped types. For most object definitions, either works. I default to interface for objects and type for everything else.
Q: Why does TypeScript widen my string literal?
A: TypeScript assumes mutable variables might change, so "admin" becomes string with let or in object properties. Use as const to preserve the literal, or declare with an explicit union type.
Q: Is the non-null assertion (!) bad practice?
A: It's not evil, but use it sparingly. Every ! is a promise that a value won't be null at runtime. If that promise is wrong, you get exactly the runtime error TypeScript exists to prevent.
Q: How do I handle libraries without types?
A: Check for @types/<library> on npm first. If nothing exists, create a .d.ts file with declare module "<library>" and basic type info. Start with any exports and refine over time.
Q: Why do I get errors when spreading objects?
A: Spread creates a new object and TypeScript can lose track of specific types. Use explicit annotations on the result, or satisfies to validate the shape.
Wrapping Up
TypeScript errors feel frustrating at first, but every one of them is preventing a potential runtime bug. The key is understanding what the compiler is telling you and knowing which patterns resolve each situation. Read errors carefully, narrow types explicitly, avoid any as a crutch, and lean on your editor's hover tooltips. Over time, these errors become less of an obstacle and more of a helpful guide.
Sources
Related Articles
AI Code Review: The Complete Guide for Engineering Teams (2026)
A definitive, practical guide to AI code review in 2026 — how it works, where it helps and where it doesn't, how to roll it out, prompt and config patterns, security trade-offs, and the metrics that prove it's working.