Raw view: Typescript
No invisible, zero-width, or bidi-control characters detected.
---
description: Enforce modern TypeScript best practices for robust, type-safe JavaScript applications, focusing on strictness, clear type definitions, and runtime validation.
globs: **/*.{js,jsx}
---
# 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:
```json
// tsconfig.json
{
"compilerOptions": {
"strict": true, // Enables all strict type-checking options
"noImplicitAny": true, // Catches untyped variables/parameters
"strictNullChecks": true, // Prevents `null`/`undefined` access without checks
"strictPropertyInitialization": true, // Ensures class properties are initialized
"allowJs": true, // Crucial for type-checking .js/.jsx files
"checkJs": true, // Enables type-checking in .js/.jsx files
// ... other options
},
"include": ["**/*.js", "**/*.jsx"] // Ensure your JS/JSX files are included
}
```
## 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.
```typescript
// types.d.ts or types.ts
interface UserProfile {
id: string;
name: string;
email: string;
age?: number; // Optional property
}
interface Point {
readonly x: number;
readonly y: number;
}
```
```javascript
// user-service.js
/**
* @typedef {import('./types').UserProfile} UserProfile
*/
/**
* @param {UserProfile} user
* @returns {string}
*/
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.
```typescript
// types.d.ts or types.ts
type ID = string | number;
type UserRole = "admin" | "user" | "guest"; // Strict literal type
type Coords = [number, number];
```
```javascript
// auth-service.js
/**
* @typedef {import('./types').ID} ID
* @typedef {import('./types').UserRole} UserRole
*/
/**
* @param {ID} userId
* @param {UserRole} role
* @returns {void}
*/
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`
```javascript
// data-processor.js
/**
* @param {any} data // Bypasses all type checks
*/
export function processDataBad(data) {
// No type safety here, data.foo might not exist
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.
```javascript
// data-processor.js
/**
* @param {unknown} data // Forces runtime checks
*/
export function processDataGood(data) {
if (typeof data === 'object' && data !== null && 'foo' in data) {
/** @type {{ foo: { bar: string } }} */
const typedData = data; // Type assertion after narrowing
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
```javascript
// utils.js
/**
* @param {unknown} value
* @returns {boolean}
*/
export function isString(value) {
return typeof value === 'string';
}
/**
* @param {unknown} value
* @returns {boolean}
*/
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.
```typescript
// types.d.ts or types.ts
interface Product {
id: string;
name: string;
price: number;
}
// Custom type guard (can be in a .ts file, or its logic used directly in .js)
/**
* @param {unknown} obj
* @returns {obj is Product}
*/
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'
);
}
```
```javascript
// api-client.js
/**
* @typedef {import('./types').Product} Product
* @type {typeof import('./types').isProduct}
*/
import { isProduct } from './types'; // Assuming isProduct is exported from types.ts/d.ts
/**
* @param {unknown} apiResponse
*/
export function handleApiResponse(apiResponse) {
if (isProduct(apiResponse)) {
// TypeScript now knows apiResponse is Product here
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
```typescript
// In a .ts file or JSDoc
enum UserStatus {
Active, // 0
Inactive, // 1
Pending // 2
}
// Problem: Can be assigned any number at runtime, e.g., UserStatus.Active = 99
```
### ✅ GOOD: String Literal Unions or `const enum`
```typescript
// types.d.ts or types.ts
/** @typedef {'active' | 'inactive' | 'pending'} UserStatus */
// Or using native TS:
type UserStatus = 'active' | 'inactive' | 'pending';
// If an enum is truly necessary, use a const string enum for compile-time safety and runtime efficiency:
const enum UserRole {
Admin = "admin",
User = "user",
Guest = "guest",
}
```
```javascript
// user-management.js
/**
* @typedef {import('./types').UserStatus} UserStatus
*/
/**
* @param {UserStatus} status
*/
export function updateUserStatus(status) {
console.log(`User status updated to: ${status}`);
}
updateUserStatus('active');
// updateUserStatus('invalid'); // Type error
```
## 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.
```javascript
// utils.js
/**
* @template T
* @param {T} arg
* @returns {T}
*/
export function identity(arg) {
return arg;
}
const num = identity(123); // num is inferred as number
const str = identity("hello"); // str is inferred as string
// With constraints (requires native TS syntax or complex JSDoc)
// For complex generics with constraints, consider writing them in a .ts file
// and importing their JSDoc type definitions.
```
## 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.
```javascript
// user-service.js
// ❌ BAD: default export
// export default class UserService { /* ... */ }
// ✅ GOOD: named export
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.
```javascript
// some-component.jsx
import React from 'react'; // Library imports
import { useSelector } from 'react-redux';
import { API_URL } from 'config/constants'; // Absolute imports (e.g., using path aliases)
import { selectUser } from 'store/selectors';
import { Button } from './components/Button'; // Relative imports
import { formatCurrency } from '../utils/formatters';
```
##