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