Top 30 Mistakes TypeScript Developers Make and How to Avoid Them

Lakin Mohapatra
6 min readJan 24, 2025

--

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 checking

Good

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(); // Risky

Good

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 error

Good

let name = user?.profile?.name ?? "Default Name"; // Safe

6. 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"; // Mutated

Good

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 shapes

Good

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 tests

Good

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)); // Fails

16. 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` type

20. 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 information

Good

declare const $: typeof jQuery; // Provides type information

22. 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"); // Allowed

Good

const colors = ["red", "green", "blue"] as const;
// colors.push("yellow"); // Error: Property 'push' does not exist

23. 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 promises

Good

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` type

28. 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 information

Good

declare const $: typeof jQuery; // Provides type information

30. 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"); // Allowed

Good

const colors = ["red", "green", "blue"] as const;
// colors.push("yellow"); // Error: Property 'push' does not exist

Final 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!

--

--

Lakin Mohapatra
Lakin Mohapatra

Written by Lakin Mohapatra

Software Engineer | Hungry coder | Proud Indian | Cyber Security Researcher | Blogger | Architect (web2 + web 3)

Responses (6)