Debugging TypeScript Applications: Lessons from the Trenches
TypeScript's type system catches a lot. But "a lot" isn't "everything." The bugs that survive the compiler are the ones that hurt the most — subtle, runti...
19 Dec 2024

TypeScript's type system catches a lot. But "a lot" isn't "everything." The bugs that survive the compiler are the ones that hurt the most — subtle, runtime, hard to reproduce.
Here's what I've learned debugging TypeScript across large codebases.
Complex type mismatches
As your types grow, intersection types can create surprises:
type User = {
id: number;
name: string;
};
type Admin = User & {
permissions: string[];
};
const getUser = (user: User) => {
console.log(user.name);
};
const admin: Admin = { id: 1, name: "Ehsan", permissions: ["read"] };
getUser(admin); // This actually works — but watch for deeper incompatibilities
When types get complex enough that the compiler gives you cryptic errors, type guards bring clarity:
const isUser = (input: any): input is User => {
return "id" in input && "name" in input;
};
if (isUser(admin)) {
getUser(admin);
}
The null problem
Enable strictNullChecks. Seriously. If you haven't, you're flying blind.
const getUserName = (user: User | null): string => {
return user.name; // Boom. Runtime error if user is null.
};
Fix it with optional chaining and nullish coalescing:
const getUserName = (user: User | null): string => {
return user?.name ?? "Guest";
};
Tools that actually help
VS Code debugger
Set up source maps in tsconfig.json:
{
"compilerOptions": {
"sourceMap": true
}
}
Add a launch config in .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/index.js"
}
]
}
Now you can set breakpoints directly in .ts files. VS Code maps them to the compiled JavaScript at runtime.
Chrome DevTools
For frontend TypeScript, compile with source maps enabled. Open Chrome DevTools, go to the Sources tab, and you'll see your original .ts files under webpack:// or similar. You can set breakpoints and inspect variables in TypeScript directly.
console.log — but smarter
I'm not above console.log. But I use it with intent.
console.table for arrays and objects:
const users: User[] = [
{ id: 1, name: "Ehsan" },
{ id: 2, name: "John" },
];
console.table(users);
String interpolation for context:
console.log(`User ID: ${user.id}, Name: ${user.name}`);
ESLint with TypeScript
Static analysis catches bugs before you even run the code:
npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn"
}
}
npx eslint src/**/*.ts
Stack traces
TypeScript stack traces can be noisy. Tools like ts-stacktrace map them back to your .ts files so you're not reading compiled JavaScript line numbers.
What I do in practice
- Remove
console.logbefore merging. Use a real logger likepinoorwinstonin production. - Write tests for the edge cases the compiler can't catch. Async race conditions, API response shapes, third-party library quirks.
- Isolate the problem. Reproduce it in the smallest possible unit before debugging the whole system.
- Read the error message. TypeScript's error messages are verbose but precise. The answer is usually in there if you read the whole thing.
Debugging is a skill you build by doing it. The compiler handles the easy bugs. The hard ones teach you the most.
Keep reading
- The Frontend Toolchain Is Now Written in Rust and Go. What That Means for You
- oxlint vs ESLint: Why I Replaced ESLint with oxlint
- SystemJS Is Dead. Native ESM Finally Won.
- Replace Axios with a Simple Custom Fetch Wrapper (Production-Ready Guide)
- Stop Shipping ChatGPT Wrappers. Ship an Agent in TypeScript, or Don't Bother.
- What Developers Are Saying About React 19: Pros and Cons