Common TypeScript Mistakes in Node.js Development and How to Avoid Them

4 min readMar 19, 2025
NodeJs + Express + Typescript

TypeScript has become the go-to language for many Node.js developers, offering type safety and improved developer experience. However, there are several common pitfalls that can lead to bugs, performance issues, or confusing code. Here are the most frequent mistakes I’ve encountered and how to address them.

1. Using `any` Type Excessively


// ❌ Bad practice
function processData(data: any) {
return data.value;
}

// ✅ Better approach
interface Data {
value: string;
}

function processData(data: Data) {
return data.value;
}

The `any` type defeats TypeScript’s purpose by bypassing type checking. Instead, use proper interfaces, type declarations, or the `unknown` type when the structure isn’t fully known but you still want type safety.

2. Ignoring Promise Error Handling

// ❌ Missing error handling
async function fetchData() {
const response = await axios.get('/api/data');
return response.data;
}

// ✅ Proper error handling
async function fetchData() {
try {
const response = await axios.get('/api/data');
return response.data;
} catch (error) {
console.error('Failed to fetch data:', error);
throw error; // Re-throw or handle appropriately
}
}

Always add proper error handling to asynchronous operations to avoid unhandled promise rejections, which can crash your Node.js application.

3. Confusing Interfaces and Types

// Both valid, but used in different scenarios
interface User {
id: number;
name: string;
}

type UserWithRole = User & {
role: string;
}

Interfaces are generally better for defining object shapes that might be extended later, while type aliases are useful for unions, intersections, and more complex types. Understanding when to use each helps create more maintainable code.

4. Misusing Type Assertions

// ❌ Dangerous assertion
const user = JSON.parse(data) as User;

// ✅ Safer approach with validation
import { z } from 'zod';

const UserSchema = z.object({
id: z.number(),
name: z.string()
});

const user = UserSchema.parse(JSON.parse(data));

Type assertions (`as Type`) tell TypeScript to trust you without verification. This can lead to runtime errors. Instead, validate data at runtime using libraries like Zod, io-ts, or class-validator.

5. Not Leveraging TypeScript’s Strict Mode

// tsconfig.json
{
"compilerOptions": {
"strict": true,
// Other settings...
}
}

Many developers disable strict mode to avoid TypeScript errors. However, enabling `strict: true` in your tsconfig.json catches potential issues like null/undefined values, implicit any types, and more rigorous type checking.

6. Incorrect Handling of Nullable Properties

// ❌ Potential runtime error
function getUsername(user: { name?: string }) {
return user.name.toLowerCase();
}

// ✅ Safe handling
function getUsername(user: { name?: string }) {
return user.name?.toLowerCase() ?? 'anonymous';
}

Always use optional chaining (`?.`) and nullish coalescing (`??`) operators when dealing with potentially undefined or null values to prevent runtime errors.

7. Using `Object` Type Instead of Proper Types

// ❌ Overly permissive
function logObject(obj: Object) {
console.log(obj.id); // TypeScript error: Property 'id' doesn't exist on type 'Object'
}

// ✅ Properly typed
function logObject(obj: { id: string }) {
console.log(obj.id); // Works fine
}

The `Object` type is very permissive and doesn’t provide useful type checking. Use specific interfaces or type declarations instead.

8. Not Using TypeScript Utility Types

/ ❌ Manually defining subset types
interface User {
id: string;
name: string;
email: string;
password: string;
}

interface UserResponse {
id: string;
name: string;
email: string;
}

// ✅ Using utility types
interface User {
id: string;
name: string;
email: string;
password: string;
}

type UserResponse = Omit<User, 'password'>;

TypeScript’s utility types like `Partial<T>`, `Omit<T, K>`, `Pick<T, K>`, and `Required<T>` can save time and reduce code duplication.

9. Not Utilizing Readonly Properties

// ❌ Mutable state
interface Config {
apiUrl: string;
timeout: number;
}

// ✅ Immutable state
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}

Use `readonly` for properties that shouldn’t change after initialization, especially for configuration objects or function parameters that should remain immutable.

10. Improper Error Typing and Handling

// ❌ Poor error handling without proper typing
async function processFile(path: string) {
try {
const data = await fs.readFile(path, 'utf8');
return JSON.parse(data);
} catch (error) {
// No type information about the error
console.log('Error occurred:', error.message);
throw error;
}
}

// ✅ Better error handling with type discrimination
async function processFile(path: string) {
try {
const data = await fs.readFile(path, 'utf8');
return JSON.parse(data);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Invalid JSON format: ${error.message}`);
} else if (error instanceof Error) {
if ('code' in error && error.code === 'ENOENT') {
throw new Error(`File not found at path: ${path}`);
}
throw new Error(`Failed to process file: ${error.message}`);
}
throw new Error('Unknown error occurred');
}
}

developers often mishandle error objects, especially with Node.js APIs. Using proper error typing with instance checks, discriminated unions, or custom error classes ensures better error handling, more informative error messages, and improved debugging experience.

Conclusion

TypeScript provides tremendous value for Node.js development, but it requires thoughtful usage to get the most benefit.

--

--

Ritesh Singh
Ritesh Singh

Written by Ritesh Singh

Full Stack Developer & Tech Consultant.

Responses (3)