Comprehensive Rules for consistent, high-quality use of TypeScript interfaces and type aliases across modern web projects (React, Node.js, ESM).
You're tired of wrestling with TypeScript's type system instead of building features. Your interfaces are inconsistent, your types are scattered across files, and every code review turns into a debate about interface vs type. Here's how to end the confusion and build TypeScript code that actually makes development faster.
Modern web development demands type safety, but most teams struggle with:
IUser, others use UserInterface, creating cognitive overheadThe result? You spend more time fixing TypeScript errors than shipping features.
These Cursor Rules implement a battle-tested approach to TypeScript interfaces and types that prioritizes developer productivity over academic purity. They establish clear patterns for when to use interfaces vs types, enforce consistent naming, and create a file structure that scales from small projects to enterprise codebases.
What makes these rules different:
interface vs type usageInstead of debating interface vs type for 10 minutes, you get instant guidance:
Consistent patterns mean reviewers focus on business logic, not TypeScript syntax:
// Before: Inconsistent, unclear intent
interface WhatIsThis extends Record<string, any> { ... }
type IUserData = { ... }
// After: Clear purpose and structure
export interface User {
id: string;
email: string;
isVerified: boolean;
}
export type UserResponse = Readonly<User>;
Composition-over-inheritance patterns mean changes stay localized instead of cascading through inheritance chains.
Before: Scattered types, manual maintenance, runtime mismatches
// Somewhere in component.tsx
const user: any = await fetchUser();
// Different file, different shape
interface UserData {
userId: number;
userName: string;
}
// Runtime breaks because API changed
After: Generated types, single source of truth, compile-time safety
// users/users.types.ts - Auto-generated from OpenAPI
export interface User {
id: string;
name: string;
email: string;
isVerified: boolean;
}
export type UserResponse = Readonly<User>;
// users/users.service.ts
import type { UserResponse } from './users.types';
export const getUser = async (id: string): Promise<UserResponse> => {
// TypeScript knows the exact shape, catches mismatches at compile time
};
Before: Props scattered, unclear contracts, runtime surprises
// Component file mixing concerns
const UserCard = ({ user, loading, onEdit }: {
user?: { id: string, name?: string };
loading: boolean;
onEdit: Function;
}) => {
// What fields does user actually have?
// When is onEdit called?
// What parameters does it expect?
};
After: Clear contracts, discoverable interfaces, predictable behavior
// users/users.types.ts
export interface User {
id: string;
name: string;
email: string;
isVerified: boolean;
}
export interface UserCardProps {
user: User | null;
isLoading: boolean;
onEdit: (userId: string) => void;
}
// components/UserCard.tsx
import type { UserCardProps } from '../users/users.types';
const UserCard = ({ user, isLoading, onEdit }: UserCardProps) => {
// Full IntelliSense, compile-time safety, clear contracts
};
Before: All-or-nothing migration attempts that fail
// Trying to convert everything at once
// 500 TypeScript errors
// Team gives up, reverts to JavaScript
After: Gradual, strategic migration with immediate value
// Step 1: Enable allowJs, convert high-value utilities
// users/users.types.ts
export interface User {
id: string;
name: string;
email: string;
}
// Step 2: Create typed islands that expand outward
// Existing JS files work unchanged
// New features get TypeScript benefits immediately
# Add the Cursor Rules to your .cursorrules file
curl -o .cursorrules https://your-cursor-rules-url
# Install essential dependencies
npm install -D typescript @types/node
// Create src/types/index.ts as your main export barrel
export type { User, UserResponse } from '../users/users.types';
export type { Product, ProductResponse } from '../products/products.types';
// Each module gets its own types file
// src/users/users.types.ts
export interface User {
id: string;
name: string;
email: string;
isVerified: boolean;
}
export type UserResponse = Readonly<User>;
export type UserCreateRequest = Omit<User, 'id' | 'isVerified'>;
Use these rules automatically via Cursor:
interfacetypetype (often readonly transformations of interfaces)// Generate from OpenAPI specs
npx openapi-generator-cli generate -i api-spec.yaml -g typescript-fetch
// Or use schema-first development with Zod
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
isVerified: z.boolean()
});
export type User = z.infer<typeof UserSchema>;
These rules don't just organize your types—they transform how your team builds software. You'll spend less time fighting TypeScript and more time solving real problems.
Ready to eliminate TypeScript chaos and accelerate your development workflow? These rules provide the foundation for sustainable, scalable TypeScript development that grows with your project.
You are an expert in TypeScript • JavaScript (ES2023) • Node.js (v22+) • React (≥18) • OpenAPI tooling • JSR.io registry.
Key Principles
- Prefer composition over inheritance to reduce complexity and tighten data-flow reasoning.
- Treat static types as the single source of truth; runtime code mirrors, never contradicts, declared interfaces.
- Use explicit, predictable state machines instead of throwing for control-flow.
- Refactor legacy JavaScript gradually: isolate typed islands → expand outward.
- Generate code where possible (OpenAPI, GraphQL, protobuf) to eliminate hand-written duplication.
- Minimize abstraction layers; remove ones that do not add tangible value.
TypeScript
- Interfaces vs Type Aliases
• Use interfaces for object/instance shapes; extend via `extends`, merge via declaration merging.
• Use `type` for unions, mapped types, conditional types, primitives, or when aliasing external types.
• Never convert between the two unless there is a concrete benefit.
- Naming Conventions
• Interface/Type names: UpperCamelCase, no `I` prefix (legacy exception: keep if project already uses).
• Serialized payloads: suffix with `Response`, `Request`, `Dto`, e.g., `UserResponse`.
• Boolean flags: prefix with auxiliary verbs, e.g., `isLoading`, `hasError`.
- Syntax & Patterns
• Prefer `interface` extension over intersection (`&`) when modelling hierarchical shapes for readability.
• Always export public types from a dedicated `types.ts` barrel file at the module root.
• Mark fields optional only when truly optional – distinguish between `undefined` and `null`.
• Prefer `readonly` for immutable properties; avoid `Partial<T>` for updates – create dedicated `UpdateXDto`.
• Use `satisfies` operator to assert literal objects against interfaces without widening.
- File Structure Example
src/
├─ users/
│ ├─ users.service.ts // business logic
│ ├─ users.controller.ts // HTTP layer
│ ├─ users.types.ts // exported interfaces & aliases
│ └─ __tests__/users.spec.ts // unit tests
Error Handling & Validation
- Validate external inputs (HTTP, CLI, env) at the boundary.
- Use dedicated validator libs (Zod, yup) to generate runtime schemas from static types or vice-versa.
- Early-return on error states:
```ts
const parseUser = (raw: unknown): Result<User, ParseError> => {
const parsed = schema.safeParse(raw);
if (!parsed.success) return Err(parsed.error);
return Ok(parsed.data);
};
```
- Reserve `throw` for truly exceptional, unrecoverable situations (e.g., infrastructure failure).
React (TSX)
- Functional components only; type props with `interface Props`.
- Use `FC<Props>` ONLY when you need generics like `ReactNode`; otherwise plain function is preferred.
- Co-locate hook return types next to the hook:
```ts
interface UseFetchState<T> { data: T | null; isLoading: boolean; error?: Error; }
export function useFetch<T>(url: string): UseFetchState<T> { … }
```
- Always memoize context value (`useMemo`) and expose typed hooks rather than raw context objects.
Vue (3 + TS)
- Use `<script setup lang="ts">` blocks; define prop types with `defineProps<{ … }>()`.
- Prefer Composition API & composables for shared logic; export generic composables with typed returns.
Angular (16+)
- Use `standalone` components; declare Input/Output generics explicitly.
- Strict template type-checking enforced: `"strictTemplates": true` in `tsconfig.app.json`.
Testing
- Write type-level tests with `expectTypeOf` (vitest) to prevent regression in public interfaces.
- Snapshot serialized DTOs (`UserResponse`) to lock contract.
Performance & Tooling
- Adopt TypeScript Native Compiler (tsc-native) for large codebases → 30-50 % compile time reduction.
- Publish packages to JSR.io; ship ESM first, CJS via conditional exports.
- Node v22+ default inspector covers ESM; no `--loader` flags needed.
- Use incremental `tsc --build` with project references for monorepos.
Security
- Never expose internal interfaces across network boundaries; export sanitized DTOs only.
- Redact secrets in type names (avoid `*SecretKey*` in public types).
Migration Checklist (Legacy JS → TS)
1. Enable `allowJs` & `checkJs` to surface type errors early.
2. Convert high-value modules to `.ts`. Start with utility, config, models.
3. Replace broad `any` with precise interfaces.
4. Tighten compiler settings gradually: `noImplicitAny` → `strictNullChecks` → `strict`.
Common Pitfalls
- Using `interface` for union types. Fix: switch to `type`.
- Overusing index signatures (`[k: string]: any`). Define explicit properties or `Record<K,V>`.
- Accidentally widening literal types – use `as const` or `satisfies`.
Reference Example
```ts
// users.types.ts
export interface User {
id: string;
name: string;
email: string;
isVerified: boolean;
}
export type UserResponse = Readonly<User>;
// service.ts
import type { UserResponse } from './users.types';
export const getUser = async (id: string): Promise<UserResponse> => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Network');
return res.json() as Promise<UserResponse>;
};
```
Adherence
- PRs failing `pnpm lint:type` (ts-eslint strict-mode) cannot be merged.
- All new exported types must include JSDoc explaining purpose and ownership.