| name | coding-standards |
| description | TypeScript + React coding standards, architecture patterns, naming conventions, ESLint configuration |
Coding Standards — TypeScript + React
Overview
Foundational rules for TypeScript + React development. Enforced in all code:
- Strict TypeScript configuration
- React best practices
- Feature-based architecture (Bulletproof React)
- Three-layer separation of concerns
- Naming conventions
- ESLint rules
TypeScript Hard Rules
These rules are NON-NEGOTIABLE. Violating any of them is a bug.
- NEVER use
any — use unknown + type guards instead
- NEVER use
as type assertions except as const
- NEVER use
! non-null assertion — use optional chaining ?. and nullish coalescing ??
- NEVER use
enum — use as const objects + derived union types
- NEVER use
@ts-ignore — use @ts-expect-error with comment if absolutely unavoidable
- ALWAYS use
interface for object shapes, type for unions/intersections
- NEVER use
I prefix on interfaces (User, not IUser)
- ALWAYS use
import type { } for type-only imports
- ALWAYS annotate all function parameters and return types explicitly
- NEVER use
export * or barrel exports >5 re-exports
- NEVER leave unused imports or variables
Configuration Requirements
tsconfig.json MUST include:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
Type Annotations
- Explicitly annotate ALL function parameters and return types, even when TypeScript could infer them
- Use modern union syntax:
T | null instead of T | undefined unless specifically undefined
- For optional properties:
prop?: T (equivalent to prop: T | undefined)
- For potentially null values: use
T | null
- Never rely on implicit type inference for public APIs
Example:
function processUser(user) {
return user.name.toUpperCase();
}
function processUser(user: User): string {
return user.name.toUpperCase();
}
const id = userId as number;
function handleUser(userId: string | number): void {
if (typeof userId === 'number') {
}
}
Interfaces vs Types
- Use
interface for object shapes (especially component props)
- Use
type for unions, intersections, tuples, primitives
- Don't extend unions or use union on interfaces
Example:
interface ButtonProps {
variant: 'primary' | 'secondary';
disabled?: boolean;
children: React.ReactNode;
}
type Status = 'idle' | 'loading' | 'success' | 'error';
type WithTimestamp = { createdAt: Date };
type User = { id: string; name: string } & WithTimestamp;
Import Organization
- Group imports: standard library, third-party, then local
- One import per line; never use wildcards (
import *)
- Separate type imports:
import type { SomeType } from '...'
- No imports inside functions — all at the top of file
Example:
import { useState } from 'react';
import type { ReactNode } from 'react';
import { QueryClient } from '@tanstack/react-query';
import type { User } from '@/types/user';
import { useAuth } from '@/features/auth';
React Hard Rules
These rules are NON-NEGOTIABLE. Violating any of them is a bug.
- NEVER use
React.FC — use named function with direct props annotation
- NEVER use
children implicitly — ALWAYS explicitly annotate children: ReactNode in props
- NEVER use
fireEvent in tests — ALWAYS use userEvent
- NEVER use
getByText as first choice — prefer screen.getByRole
- NEVER use
getByTestId without strong justification
- NEVER use bare
onClick handlers without specific types
- NEVER use
any for event handlers — use React-specific types
- NEVER use
ref.current! — use optional chaining or proper type narrowing
- NEVER create Context without a custom hook to access it
- NEVER use global Context default of
undefined — throw in the hook instead
Component Props
- Use direct props annotation, never
React.FC<Props>
- Explicitly include
children: React.ReactNode if component accepts children
- Use
Props suffix for interfaces (ButtonProps, CardProps)
Example:
const Button: React.FC<{ label: string; onClick: () => void }> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
interface ButtonProps {
label: string;
onClick: () => void;
children?: React.ReactNode;
disabled?: boolean;
}
function Button({ label, onClick, children, disabled }: ButtonProps): React.ReactElement {
return (
<button onClick={onClick} disabled={disabled}>
{label} {children}
</button>
);
}
Event Handler Types
- Use specific React event types:
React.MouseEvent<HTMLButtonElement>, React.ChangeEvent<HTMLInputElement>
- Never use
any for event parameters
- Type handlers in props with specific event types
Example:
interface FormProps {
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
function Form({ onSubmit, onChange }: FormProps): React.ReactElement {
return (
<form onSubmit={onSubmit}>
<input onChange={onChange} />
</form>
);
}
Hooks Rules
- Never call hooks inside conditions, loops, or nested functions
- Always use hooks at the top level of a function component
- Custom hooks must start with
use prefix
- List all dependencies in dependency arrays — never omit or use empty array to suppress warnings
Example:
function Component({ shouldFetch }: { shouldFetch: boolean }): React.ReactElement {
if (shouldFetch) {
const data = useFetch();
}
return <div />;
}
function Component({ shouldFetch }: { shouldFetch: boolean }): React.ReactElement {
const data = useFetch();
if (!shouldFetch) return <div />;
return <div>{data}</div>;
}
Context Patterns
- Default value in
createContext should be null or undefined
- Create a custom hook to access context — this hook should throw if context is not available
- Never use context as a component wrapper without explicit exports
Example:
const ThemeContext = React.createContext<Theme | undefined>(undefined);
const ThemeContext = React.createContext<Theme | null>(null);
function useTheme(): Theme {
const theme = React.useContext(ThemeContext);
if (theme === null) {
throw new Error('useTheme must be used within ThemeProvider');
}
return theme;
}
interface ThemeProviderProps {
children: React.ReactNode;
}
function ThemeProvider({ children }: ThemeProviderProps): React.ReactElement {
const [theme, setTheme] = React.useState<Theme>(defaultTheme);
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
Feature-Based Architecture
Directory Structure
src/
├── app/ # Shell: routes, providers, entry point
│ ├── App.tsx # Main route component
│ └── Root.tsx # Root with providers
├── features/ # Self-contained feature modules
│ ├── auth/
│ │ ├── api/ # API calls + TanStack Query hooks
│ │ ├── components/ # Feature-scoped UI
│ │ ├── hooks/ # Feature-scoped logic hooks
│ │ ├── stores/ # Feature-scoped Zustand stores
│ │ ├── types/ # Feature-scoped types
│ │ └── index.ts # Public API (max 5 exports)
│ ├── dashboard/
│ │ ├── api/
│ │ ├── components/
│ │ └── ...
│ └── users/
│ └── ...
├── components/ # Shared UI (Button, Modal, Card)
│ └── ui/ # Primitives (shadcn/ui style)
├── hooks/ # Shared hooks (useDebounce, useMediaQuery)
├── lib/ # Preconfigured libs (axios instance, cn utility)
├── stores/ # Global state stores
├── types/ # Shared TypeScript types
├── utils/ # Pure utility functions
└── test/ # Test setup, custom render, MSW handlers
Dependency Rules (NON-NEGOTIABLE)
Direction: shared -> features -> app
- ✅ Features can import from
shared/ (hooks, utils, components, types, lib, stores)
- ✅ App can import from
features/ and shared/
- ❌ Features NEVER import from other features
- ❌ Shared code NEVER imports from features or app
Cross-feature composition happens ONLY in app/ (root routes/pages).
Example:
import { formatDate } from '@/utils/format';
import { Button } from '@/components/ui/button';
import type { User } from '@/types/user';
import { useOrders } from '@/features/orders/hooks';
import { AuthPage } from '@/features/auth';
import { DashboardPage } from '@/features/dashboard';
Three-Layer Separation of Concerns
Layer 1: API/Service Layer
- Pure functions, no React imports
- Fully testable without rendering
- Located in
features/<name>/api/<name>Api.ts
- Exported in
features/<name>/api/queries.ts for TanStack Query
Layer 2: Logic/Hook Layer
- Custom hooks bridging React state and services
- Located in
features/<name>/hooks/
- Handles API calls, state management, side effects
Layer 3: UI/Component Layer
- Rendering only, minimal logic
- Located in
features/<name>/components/
- Receives data from hooks, calls handlers
Example:
export async function fetchUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`);
return response.data;
}
export function useUser(id: string): {
user: User | undefined;
isLoading: boolean;
error: Error | null;
} {
const { data: user, isLoading, error } = useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
});
return { user, isLoading, error };
}
interface UserCardProps {
userId: string;
}
function UserCard({ userId }: UserCardProps): React.ReactElement {
const { user, isLoading } = useUser(userId);
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}
Naming Conventions
| Element | Convention | Example |
|---|
| Directories | kebab-case | user-profile/, auth-modal/ |
| Component files | PascalCase.tsx | UserProfile.tsx, LoginForm.tsx |
| Component exports | PascalCase function | export function UserProfile() {} |
| Hook files | camelCase with use prefix | useAuth.ts, useFormData.ts |
| Hook exports | camelCase with use prefix | export function useAuth() |
| Utility functions | camelCase | formatDate.ts, calculateTotal.ts |
| Types/Interfaces | PascalCase | User, ButtonProps, FormData |
| Constants | UPPER_SNAKE_CASE | API_BASE_URL, MAX_RETRIES |
| Const objects | PascalCase + as const | const OrderStatus = {...} as const |
| Test files | .test.tsx for components, .spec.ts for e2e | Button.test.tsx, auth.spec.ts |
Props Naming
- Always use
Props suffix: ButtonProps, CardProps, ModalProps
- Optional props use
?: disabled?: boolean
- Event handlers start with
on: onClick, onChange, onSubmit
Example:
interface UserProfileProps {
userId: string;
onLogout?: () => void;
disabled?: boolean;
}
function UserProfile({ userId, onLogout, disabled }: UserProfileProps): React.ReactElement {
}
ESLint Configuration
Required Rules
export default [
{
files: ['**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' }
],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/explicit-function-return-types': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-var': 'error',
'prefer-const': 'error',
},
},
];
Recommended Plugins
@typescript-eslint/eslint-plugin with strict-type-checked + stylistic-type-checked
eslint-plugin-react-hooks
eslint-plugin-import for import ordering
- Biome as alternative if detected in project
Path Aliases
All imports from src/ use @/ instead of relative paths.
tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
vite.config.ts:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Always use path aliases:
import { Button } from '../../../../components/ui/button';
import { Button } from '@/components/ui/button';
Code Organization
File Ordering in Components
- Imports (standard library, third-party, local)
- Types/Interfaces
- Component function
- Exports
Example:
import { useState } from 'react';
import type { ReactNode } from 'react';
import clsx from 'clsx';
import type { User } from '@/types/user';
import { Button } from '@/components/ui/button';
interface UserCardProps {
user: User;
onSelect: (id: string) => void;
}
export function UserCard({ user, onSelect }: UserCardProps): ReactNode {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={clsx('p-4', isHovered && 'bg-gray-100')}
>
<h3>{user.name}</h3>
<Button onClick={() => onSelect(user.id)}>Select</Button>
</div>
);
}
Documentation
- Keep comments minimal and explanatory (WHY, not WHAT)
- Write concise docstrings for public APIs
- Document component props in interface definition
- Never change existing comments unless fixing the issue they describe
Example:
export function formatUserName(
firstName: string | null,
lastName: string | null,
): string {
if (firstName && lastName) return `${firstName} ${lastName}`;
return firstName ?? lastName ?? 'Unknown User';
}
Summary
All code must follow these standards:
- ✅ Strict TypeScript with no
any, as, !
- ✅ React components with explicit props, no
React.FC
- ✅ Feature-based architecture with dependency rules
- ✅ Path aliases for all imports
- ✅ Clear naming conventions
- ✅ ESLint configured and enforced
- ✅ Three-layer separation (API, Hook, UI)