Top 30 Mistakes TypeScript Developers Make and How to Avoid Them
TypeScript has become one of the most popular languages for building scalable and maintainable applications. Its static typing system, combined with JavaScript’s flexibility, makes it a powerful tool for developers. However, even experienced developers can fall into common pitfalls that reduce the effectiveness of TypeScript.
In this article, we’ll explore 30 common mistakes TypeScript developers make and provide practical solutions to avoid or fix them.
1. Using any Excessively
Mistake
Overusing any removes type safety, making your code prone to runtime errors.
Bad
let data: any;
data = "Hello";
data = 42; // No type checkingGood
let data: unknown;
data = "Hello";
if (typeof data === "string") {
console.log(data.toUpperCase()); // Safe to use
}2. Ignoring Strict Compiler Options
Mistake
Not enabling strict mode weakens TypeScript’s type checking.
Bad
{
"compilerOptions": {
"strict": false
}
}Good
{
"compilerOptions": {
"strict": true
}
}3. Not Using Type Inference
Mistake
Explicitly typing everything, even when TypeScript can infer it.
Bad
let count: number = 0;
let name: string = "John";Good
let count = 0; // TypeScript infers `number`
let name = "John"; // TypeScript infers `string`4. Overusing Non-Null Assertions (!)
Mistake
Using ! to assert non-null values without proper checks.
Bad
let element = document.getElementById("myElement")!;
element.click(); // RiskyGood
let element = document.getElementById("myElement");
if (element) {
element.click(); // Safe
}5. Not Handling undefined or null Properly
Mistake
Ignoring potential undefined or null values.
Bad
let name = user.profile.name; // Could throw an errorGood
let name = user?.profile?.name ?? "Default Name"; // Safe6. Misusing Enums
Mistake
Using enums when a union type would suffice.
Bad
enum Status {
Active,
Inactive,
}Good
type Status = "active" | "inactive";7. Not Leveraging Utility Types
Mistake
Manually creating types when utility types could simplify your code.
Bad
interface User {
id: number;
name: string;
email: string;
}interface UserPreview {
id: number;
name: string;
}Good
type UserPreview = Pick<User, "id" | "name">;8. Ignoring readonly for Immutability
Mistake
Not marking properties as readonly when they shouldn’t change.
Bad
interface Config {
apiUrl: string;
}
const config: Config = { apiUrl: "https://api.example.com" };
config.apiUrl = "https://malicious.com"; // MutatedGood
interface Config {
readonly apiUrl: string;
}
const config: Config = { apiUrl: "https://api.example.com" };
// config.apiUrl = "https://malicious.com"; // Error: Cannot assign to 'apiUrl'9. Not Using Generics Effectively
Mistake
Writing repetitive code instead of using generics.
Bad
function identityNumber(num: number): number {
return num;
}
function identityString(str: string): string {
return str;
}Good
function identity<T>(value: T): T {
return value;
}10. Ignoring interface vs type Differences
Mistake
Using interface and type interchangeably without understanding their differences.
Bad
type User = {
name: string;
};
type Admin = User & { role: string }; // Works, but `interface` is better for object shapesGood
interface User {
name: string;
}
interface Admin extends User {
role: string;
}11. Not Using as const for Literal Types
Mistake
Not preserving literal types when needed.
Bad
const colors = ["red", "green", "blue"]; // Type: string[]Good
const colors = ["red", "green", "blue"] as const; // Type: readonly ["red", "green", "blue"]12. Not Handling Async Code Properly
Mistake
Forgetting to handle promises or using any for async results.
Bad
function fetchData(): Promise<any> {
return fetch("/api/data");
}Good
async function fetchData(): Promise<MyDataType> {
const response = await fetch("/api/data");
return response.json();
}13. Not Using Type Guards
Mistake
Not narrowing types with type guards.
Bad
function printValue(value: unknown) {
console.log(value.toUpperCase()); // Error: 'value' is of type 'unknown'
}Good
function isString(value: unknown): value is string {
return typeof value === "string";
}function printValue(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // Safe
}
}14. Ignoring tsconfig.json Settings
Mistake
Not configuring tsconfig.json properly.
Bad
{
"compilerOptions": {
"target": "ES5"
}
}Good
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}15. Not Writing Tests for Types
Mistake
Assuming types are correct without testing them.
Bad
function add(a: number, b: number): number {
return a + b;
}
// No type testsGood
import { expectType } from "tsd";function add(a: number, b: number): number {
return a + b;
}
expectType<number>(add(1, 2)); // Passes
expectType<string>(add(1, 2)); // Fails16. Not Using keyof for Type-Safe Object Keys
Mistake
Accessing object keys without type safety can lead to runtime errors.
Bad
function getValue(obj: any, key: string) {
return obj[key]; // No type safety
}Good
function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // Type-safe
}17. Ignoring never for Exhaustiveness Checking
Mistake
Not using never to ensure all cases are handled in a union type.
Bad
type Shape = "circle" | "square";function getArea(shape: Shape) {
if (shape === "circle") {
return Math.PI * 2 ** 2;
}
// Forgot to handle "square"
}Good
function getArea(shape: Shape) {
if (shape === "circle") {
return Math.PI * 2 ** 2;
}
if (shape === "square") {
return 4 ** 2;
}
const _exhaustiveCheck: never = shape; // Ensures all cases are handled
throw new Error(`Unknown shape: ${shape}`);
}18. Not Using Mapped Types
Mistake
Manually creating similar types instead of using mapped types.
Bad
interface User {
id: number;
name: string;
email: string;
}interface OptionalUser {
id?: number;
name?: string;
email?: string;
}Good
type OptionalUser = Partial<User>;19. Not Using satisfies for Type Validation
Mistake
Not validating that an object satisfies a specific type.
Bad
const user = {
id: 1,
name: "John",
// Missing `email`
};Good
const user = {
id: 1,
name: "John",
email: "john@example.com",
} satisfies User; // Ensures `user` matches `User` type20. Not Using infer in Conditional Types
Mistake
Not leveraging infer to extract types dynamically.
Bad
type GetReturnType<T> = T extends (...args: any[]) => any ? any : never;Good
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;21. Not Using declare for Ambient Declarations
Mistake
Not using declare for external libraries or global variables.
Bad
const $ = jQuery; // No type informationGood
declare const $: typeof jQuery; // Provides type information22. Not Using const Assertions for Immutable Arrays/Objects
Mistake
Not using const assertions to make arrays or objects immutable.
Bad
const colors = ["red", "green", "blue"];
colors.push("yellow"); // AllowedGood
const colors = ["red", "green", "blue"] as const;
// colors.push("yellow"); // Error: Property 'push' does not exist23. Not Using this Parameter in Callbacks
Mistake
Not binding this in callbacks, leading to unexpected behavior.
Bad
class Button {
constructor() {
this.element.addEventListener("click", this.handleClick);
}
handleClick() {
console.log(this); // `this` is undefined
}
}Good
class Button {
constructor() {
this.element.addEventListener("click", this.handleClick.bind(this));
}
handleClick() {
console.log(this); // `this` refers to the Button instance
}
}24. Not Using Record for Dictionary-Like Objects
Mistake
Using any or loose types for dictionary-like objects.
Bad
const users: { [key: string]: any } = {
"1": { name: "John" },
"2": { name: "Jane" },
};Good
const users: Record<string, { name: string }> = {
"1": { name: "John" },
"2": { name: "Jane" },
};25. Not Using Awaited for Unwrapping Promises
Mistake
Not properly unwrapping nested promises.
Bad
type Result = Promise<Promise<string>>; // Nested promisesGood
type Result = Awaited<Promise<Promise<string>>>; // Unwraps to `string`26. Not Using unknown for Catch Clauses
Mistake
Using any for catch clauses, which can hide errors.
Bad
try {
// Some code
} catch (error: any) {
console.log(error.message); // Unsafe
}Good
try {
// Some code
} catch (error: unknown) {
if (error instanceof Error) {
console.log(error.message); // Safe
}
}27. Not Using satisfies for Type Validation
Mistake
Not validating that an object satisfies a specific type.
Bad
const user = {
id: 1,
name: "John",
// Missing `email`
};Good
const user = {
id: 1,
name: "John",
email: "john@example.com",
} satisfies User; // Ensures `user` matches `User` type28. Not Using infer in Conditional Types
Mistake
Not leveraging infer to extract types dynamically.
Bad
type GetReturnType<T> = T extends (...args: any[]) => any ? any : never;Good
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;29. Not Using declare for Ambient Declarations
Mistake
Not using declare for external libraries or global variables.
Bad
const $ = jQuery; // No type informationGood
declare const $: typeof jQuery; // Provides type information30. Not Using const Assertions for Immutable Arrays/Objects
Mistake
Not using const assertions to make arrays or objects immutable.
Bad
const colors = ["red", "green", "blue"];
colors.push("yellow"); // AllowedGood
const colors = ["red", "green", "blue"] as const;
// colors.push("yellow"); // Error: Property 'push' does not existFinal Thoughts :
By avoiding these common mistakes and following the Good practices, you can write more robust, maintainable, and type-safe TypeScript code.
TypeScript is not just about adding types — it’s about leveraging the type system to catch errors early and improve code quality.
Happy coding!
