Frontend
Modern TypeScript patterns and type-safe coding practices for robust applications.
Copy this complete rule and save it as a .mdc file in your .cursor/rules directory
# TypeScript Best Practices
## Type Inference
### Let TypeScript Infer When Possible
```typescript
// Unnecessary explicit type
const name: string = 'John';
// Let TypeScript infer
const name = 'John'; // inferred as string
// Explicit types are needed for function parameters
function greet(name: string): string {
return `Hello, ${name}`;
}
```
### Use `const` Assertions
```typescript
// Without const assertion - type is string[]
const roles = ['admin', 'user', 'guest'];
// With const assertion - type is readonly ['admin', 'user', 'guest']
const roles = ['admin', 'user', 'guest'] as const;
// Useful for creating union types
type Role = typeof roles[number]; // 'admin' | 'user' | 'guest'
```
## Interfaces vs Types
### Use Interfaces for Objects
```typescript
// Interfaces are extendable and show better error messages
interface User {
id: string;
name: string;
email: string;
}
interface AdminUser extends User {
permissions: string[];
}
```
### Use Types for Unions and Utilities
```typescript
type Status = 'pending' | 'active' | 'archived';
type Nullable<T> = T | null;
type UserOrAdmin = User | AdminUser;
```
## Utility Types
### Common Patterns
```typescript
interface User {
id: string;
name: string;
email: string;
password: string;
}
// Partial - all properties optional
type UpdateUser = Partial<User>;
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit - exclude properties
type PublicUser = Omit<User, 'password'>;
// Required - make all properties required
type RequiredUser = Required<Partial<User>>;
// Readonly - immutable properties
type ImmutableUser = Readonly<User>;
// Record - key-value mapping
type UserRoles = Record<string, Role>;
```
## Generics
### Basic Generic Functions
```typescript
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // number
const str = first(['a', 'b']); // string
```
### Constrained Generics
```typescript
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
```
### Generic Components (React)
```typescript
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
// Usage
<List items={users} renderItem={(user) => <li>{user.name}</li>} />
```
## Null Safety
### Use Strict Null Checks
```typescript
// tsconfig.json: "strictNullChecks": true
function getUser(id: string): User | null {
// ...
}
const user = getUser('123');
// user.name; // Error: Object is possibly null
if (user) {
user.name; // OK - type narrowed
}
// Optional chaining
const name = user?.name;
// Nullish coalescing
const displayName = user?.name ?? 'Anonymous';
```
### Non-Null Assertion (Use Sparingly)
```typescript
// Only use when you're 100% certain
const element = document.getElementById('app')!;
// Better: Handle the null case
const element = document.getElementById('app');
if (!element) throw new Error('App element not found');
```
## Discriminated Unions
```typescript
interface LoadingState {
status: 'loading';
}
interface SuccessState<T> {
status: 'success';
data: T;
}
interface ErrorState {
status: 'error';
error: string;
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'loading':
return <Spinner />;
case 'success':
return <Data data={state.data} />; // data is available
case 'error':
return <Error message={state.error} />; // error is available
}
}
```
## Type Guards
### Custom Type Guards
```typescript
interface Dog {
type: 'dog';
bark(): void;
}
interface Cat {
type: 'cat';
meow(): void;
}
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return pet.type === 'dog';
}
function handlePet(pet: Pet) {
if (isDog(pet)) {
pet.bark(); // TypeScript knows pet is Dog
} else {
pet.meow(); // TypeScript knows pet is Cat
}
}
```
### Assertion Functions
```typescript
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}
function processInput(input: unknown) {
assertIsString(input);
// TypeScript knows input is string here
console.log(input.toUpperCase());
}
```
## Async Patterns
```typescript
// Typed async functions
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
// Typed error handling
interface ApiError {
code: string;
message: string;
}
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.message);
}
return response.json();
}
```
## Configuration
### Recommended tsconfig.json
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
```
## Anti-Patterns to Avoid
```typescript
// DON'T: Use `any`
function process(data: any) {} // Defeats the purpose of TypeScript
// DO: Use `unknown` and narrow
function process(data: unknown) {
if (typeof data === 'string') {
// Now safely use as string
}
}
// DON'T: Overuse type assertions
const user = {} as User; // Lies to TypeScript
// DO: Properly construct objects
const user: User = {
id: '1',
name: 'John',
email: '[email protected]'
};
// DON'T: Use enums (they have quirks)
enum Status { Active, Inactive }
// DO: Use const objects or union types
const Status = { Active: 'active', Inactive: 'inactive' } as const;
type Status = typeof Status[keyof typeof Status];
```
What this rule helps you achieve