| name | typescript-cursor_mdc |
| description | [Applies to: **/*.{js,jsx}] Enforce modern TypeScript best practices for robust, type-safe JavaScript applications, focusing on strictness, clear type definitions, and runtime validation. |
| source | cursor_mdc |
TypeScript Best Practices (for JS/JSX Type-Checking)
This guide outlines essential TypeScript best practices for teams working with JavaScript or JSX files that are type-checked by TypeScript (e.g., via tsconfig.json with allowJs and/or JSDoc annotations). While many examples use native TypeScript syntax for clarity and conciseness, the underlying principles and type-safety benefits apply directly to your .js/.jsx codebase.
1. Enable Strict Mode in tsconfig.json
This is the single most impactful change you can make. strict: true enables a suite of crucial checks that catch the vast majority of common type-related bugs at compile time.
Action: Ensure your tsconfig.json includes:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"allowJs": true,
"checkJs": true,
},
"include": ["**/*.js", "**/*.jsx"]
}
2. Define Clear Type Contracts
Use interfaces and type aliases to describe the shape of your data, especially for API payloads, component props, and complex objects. For .js/.jsx files, leverage JSDoc to apply these types.
✅ GOOD: Interfaces for Object Shapes & Classes
Interfaces are ideal for defining the shape of objects and for implementing explicit contracts when working with classes. Define these in .d.ts or .ts files, then reference them in JSDoc.
interface UserProfile {
id: string;
name: string;
email: string;
age?: number;
}
interface Point {
readonly x: number;
readonly y: number;
}
export function greetUser(user) {
return `Hello, ${user.name}!`;
}
✅ GOOD: Type Aliases for Complex Types & Unions
Type aliases are powerful for defining unions, intersections, primitive aliases, and tuples. Define these in .d.ts or .ts files, then reference them in JSDoc.
type ID = string | number;
type UserRole = "admin" | "user" | "guest";
type Coords = [number, number];
export function assignRole(userId, role) {
console.log(`User ${userId} assigned role: ${role}`);
}
3. Avoid any and Prefer unknown for Untyped Data
any completely bypasses TypeScript's checks, reintroducing JavaScript's runtime errors. unknown is a safer alternative that forces you to narrow the type before use.
❌ BAD: Using any
export function processDataBad(data) {
console.log(data.foo.bar);
}
✅ GOOD: Using unknown (and Type Guards)
When dealing with data from external sources (e.g., API responses), use unknown and then narrow its type using runtime checks.
export function processDataGood(data) {
if (typeof data === 'object' && data !== null && 'foo' in data) {
const typedData = data;
console.log(typedData.foo.bar);
} else {
console.error('Invalid data structure');
}
}
4. Implement Robust Runtime Type Validation (Type Guards)
TypeScript's compile-time checks are erased at runtime. For data from external sources (APIs, user input), you must perform runtime validation to prevent crashes.
✅ GOOD: typeof for Primitives
export function isString(value) {
return typeof value === 'string';
}
export function isNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
✅ GOOD: Custom Type Guards for Complex Objects
For interfaces or complex object shapes, create custom functions that perform runtime checks and act as type predicates.
interface Product {
id: string;
name: string;
price: number;
}
function isProduct(obj) {
return (
typeof obj === 'object' && obj !== null &&
'id' in obj && typeof obj.id === 'string' &&
'name' in obj && typeof obj.name === 'string' &&
'price' in obj && typeof obj.price === 'number'
);
}
import { isProduct } from './types';
export function handleApiResponse(apiResponse) {
if (isProduct(apiResponse)) {
console.log(`Fetched product: ${apiResponse.name} at $${apiResponse.price}`);
} else {
console.error('API response is not a valid Product:', apiResponse);
}
}
5. Prefer Union Types over Traditional Enums
For simple sets of related constants, strict literal union types ("admin" | "user") are generally preferred over TypeScript's enum keyword because they offer better type safety and simpler runtime representation. If you must use enums, prefer const enum or string enums.
❌ BAD: Numeric Enums
enum UserStatus {
Active,
Inactive,
Pending
}
✅ GOOD: String Literal Unions or const enum
type UserStatus = 'active' | 'inactive' | 'pending';
const enum UserRole {
Admin = "admin",
User = "user",
Guest = "guest",
}
export function updateUserStatus(status) {
console.log(`User status updated to: ${status}`);
}
updateUserStatus('active');
6. Use Generics for Reusable Components/Functions
Generics allow you to write flexible and reusable code that works with a variety of types while maintaining type safety.
export function identity(arg) {
return arg;
}
const num = identity(123);
const str = identity("hello");
7. Enforce Consistent Code Organization
Maintainable codebases rely on clear structure.
✅ GOOD: Named Exports (No Default Exports)
Named exports promote explicit imports and make refactoring easier. Avoid default exports entirely.
export class UserService { }
export const DEFAULT_USER = { };
✅ GOOD: Organized Imports
Group imports by type (e.g., library, absolute path, relative path) and sort them alphabetically. Use path aliases for cleaner imports from deeply nested modules.
import React from 'react';
import { useSelector } from 'react-redux';
import { API_URL } from 'config/constants';
import { selectUser } from 'store/selectors';
import { Button } from './components/Button';
import { formatCurrency } from '../utils/formatters';