Modern Project Structure Implementation
1. Monorepo Nx Lerna Workspace Setup
Manage multiple related packages in a single repository with shared dependencies, unified tooling, and efficient builds.
| Tool | Key Features | Best For | Learning Curve |
|---|---|---|---|
| Nx | Smart rebuilds, computation caching, code generators, dependency graph | Large teams, enterprise apps | Medium-High |
| Turborepo | Fast builds, remote caching, simple config, Vercel integration | Modern apps, Vercel users | Low-Medium |
| Lerna | Package versioning, publishing, bootstrap, legacy support | Library authors, npm packages | Medium |
| npm/yarn/pnpm workspaces | Built-in, simple, no extra tools | Small-medium projects | Low |
Example: Nx monorepo setup with React and shared libraries
// Initialize Nx workspace
npx create-nx-workspace@latest myorg
// Structure
myorg/
├── apps/
│ ├── web/ # Next.js app
│ ├── mobile/ # React Native app
│ └── admin/ # Admin dashboard
├── libs/
│ ├── shared/
│ │ ├── ui/ # Shared UI components
│ │ ├── utils/ # Utility functions
│ │ └── types/ # TypeScript types
│ ├── features/
│ │ ├── auth/ # Authentication feature
│ │ └── payments/ # Payment feature
│ └── data-access/
│ └── api/ # API client
├── tools/ # Custom scripts
├── nx.json # Nx configuration
├── tsconfig.base.json # Base TypeScript config
└── package.json
// nx.json - Configuration
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
}
}
}
// tsconfig.base.json - Path mapping
{
"compilerOptions": {
"paths": {
"@myorg/shared/ui": ["libs/shared/ui/src/index.ts"],
"@myorg/shared/utils": ["libs/shared/utils/src/index.ts"],
"@myorg/features/auth": ["libs/features/auth/src/index.ts"]
}
}
}
// Generate new library
nx generate @nx/react:library shared/ui
// Generate new app
nx generate @nx/next:application admin
// Run commands
nx build web # Build web app
nx test shared-ui # Test UI library
nx run-many --target=test --all # Test all projects
nx affected:build # Build only affected by changes
nx dep-graph # Visualize dependency graph
Example: Turborepo setup (simpler alternative)
// Initialize Turborepo
npx create-turbo@latest
// Structure
my-turborepo/
├── apps/
│ ├── web/ # Next.js app
│ └── docs/ # Documentation site
├── packages/
│ ├── ui/ # Shared UI components
│ ├── eslint-config/ # Shared ESLint config
│ ├── tsconfig/ # Shared TS configs
│ └── typescript-config/
├── turbo.json # Turbo configuration
└── package.json
// turbo.json - Pipeline configuration
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["build"],
"cache": false
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
// Root package.json
{
"name": "my-turborepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
}
}
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"exports": {
".": "./src/index.tsx",
"./button": "./src/Button.tsx"
}
}
// apps/web - Import from workspace
import { Button } from '@repo/ui';
import { formatDate } from '@repo/utils';
Example: pnpm workspaces (lightweight approach)
// pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
// Root package.json
{
"name": "monorepo",
"private": true,
"scripts": {
"build": "pnpm -r --filter='./packages/*' build",
"test": "pnpm -r test",
"dev": "pnpm --parallel -r dev"
}
}
// packages/shared/package.json
{
"name": "@monorepo/shared",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@monorepo/shared": "workspace:*"
}
}
// Commands
pnpm install # Install all deps
pnpm --filter web dev # Run web app
pnpm --filter @monorepo/shared build # Build shared package
pnpm -r build # Build all packages recursively
Choosing a Tool: Use Nx for large teams with complex
dependencies.
Use Turborepo for modern apps with simple needs. Use pnpm
workspaces
for minimal overhead. Use Lerna for publishing npm packages.
2. Feature-based Folder Structure
Organize code by feature/domain rather than technical layer for better scalability and team collaboration.
| Structure Type | Organization | Pros | Cons |
|---|---|---|---|
| Feature-based | Group by business feature | Scalable, clear ownership, easy to delete | Shared code requires careful planning |
| Layer-based | Group by technical layer (components, hooks, utils) | Simple, familiar, flat structure | Hard to scale, unclear ownership |
| Hybrid | Features + shared folder for common code | Best of both worlds | More complex initially |
Example: Feature-based structure (recommended for large apps)
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ ├── SignupForm.tsx
│ │ │ └── PasswordReset.tsx
│ │ ├── hooks/
│ │ │ ├── useAuth.ts
│ │ │ └── useLogin.ts
│ │ ├── api/
│ │ │ └── authApi.ts
│ │ ├── types/
│ │ │ └── auth.types.ts
│ │ ├── utils/
│ │ │ └── validation.ts
│ │ └── index.ts # Barrel export
│ │
│ ├── dashboard/
│ │ ├── components/
│ │ │ ├── DashboardLayout.tsx
│ │ │ ├── StatsCard.tsx
│ │ │ └── RecentActivity.tsx
│ │ ├── hooks/
│ │ │ └── useDashboardData.ts
│ │ ├── api/
│ │ │ └── dashboardApi.ts
│ │ └── index.ts
│ │
│ ├── products/
│ │ ├── components/
│ │ │ ├── ProductList.tsx
│ │ │ ├── ProductCard.tsx
│ │ │ └── ProductDetail.tsx
│ │ ├── hooks/
│ │ │ ├── useProducts.ts
│ │ │ └── useProductFilters.ts
│ │ ├── api/
│ │ │ └── productsApi.ts
│ │ ├── store/
│ │ │ └── productsSlice.ts
│ │ └── index.ts
│ │
│ └── cart/
│ ├── components/
│ ├── hooks/
│ ├── api/
│ └── index.ts
│
├── shared/
│ ├── components/ # Reusable UI components
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Modal/
│ │ └── Layout/
│ ├── hooks/ # Generic hooks
│ │ ├── useDebounce.ts
│ │ ├── useLocalStorage.ts
│ │ └── useMediaQuery.ts
│ ├── utils/ # Generic utilities
│ │ ├── format.ts
│ │ ├── validation.ts
│ │ └── api.ts
│ ├── types/ # Global types
│ │ └── common.types.ts
│ └── constants/
│ └── config.ts
│
├── pages/ # Next.js pages or React Router routes
│ ├── index.tsx
│ ├── dashboard.tsx
│ └── products/
│ ├── [id].tsx
│ └── index.tsx
│
├── styles/ # Global styles
│ ├── globals.css
│ └── variables.css
│
├── lib/ # External library configs
│ ├── axios.ts
│ └── react-query.ts
│
└── App.tsx
Example: Colocation pattern (keep related files together)
// Colocate tests, stories, styles with components
src/features/auth/components/LoginForm/
├── LoginForm.tsx
├── LoginForm.test.tsx
├── LoginForm.stories.tsx
├── LoginForm.module.css
├── useLoginForm.ts # Component-specific hook
└── index.ts
// LoginForm/index.ts - Barrel export
export { LoginForm } from './LoginForm';
export type { LoginFormProps } from './LoginForm';
// Usage in other files
import { LoginForm } from '@/features/auth/components/LoginForm';
// Alternative flat structure for simple components
src/features/auth/components/
├── LoginForm.tsx
├── LoginForm.test.tsx
├── LoginForm.stories.tsx
├── SignupForm.tsx
└── SignupForm.test.tsx
Example: Next.js App Router with features
app/
├── (auth)/ # Route group
│ ├── login/
│ │ └── page.tsx # Uses LoginForm from features/auth
│ └── signup/
│ └── page.tsx
│
├── dashboard/
│ ├── page.tsx # Uses Dashboard from features/dashboard
│ ├── layout.tsx
│ └── settings/
│ └── page.tsx
│
├── products/
│ ├── page.tsx
│ └── [id]/
│ └── page.tsx
│
├── api/
│ ├── auth/
│ │ └── route.ts
│ └── products/
│ └── route.ts
│
├── layout.tsx
└── page.tsx
// Separation of concerns:
// - features/ = business logic, UI, hooks
// - app/ = routing, server components, metadata
// - lib/ = configurations, clients
Anti-pattern: Don't create deep nesting (max 3-4 levels). Don't mix feature code with shared
code. Don't use index.ts for everything (explicit imports are clearer). Avoid circular dependencies between
features.
3. Barrel Exports Index Files
Use index.ts files to create clean public APIs for modules and simplify imports, but avoid performance pitfalls.
| Pattern | Use Case | Benefits | Cautions |
|---|---|---|---|
| Feature Barrel | Export feature public API | Clean imports, encapsulation | Can break tree-shaking |
| Component Barrel | Export component + types | Single import point | Import cost for all |
| Namespace Barrel | Group related utilities | Organized exports | All-or-nothing imports |
| Direct Exports | Performance-critical code | Best tree-shaking | Longer import paths |
Example: Feature barrel export pattern
// features/auth/index.ts - Public API
export { LoginForm } from './components/LoginForm';
export { SignupForm } from './components/SignupForm';
export { useAuth } from './hooks/useAuth';
export { useLogin } from './hooks/useLogin';
export type { User, AuthState, LoginCredentials } from './types/auth.types';
// Don't export internal utilities
// import { validateEmail } from './utils/validation'; ❌
// Consumer code
import { LoginForm, useAuth } from '@/features/auth';
// features/auth/components/index.ts - Component barrel
export { LoginForm } from './LoginForm/LoginForm';
export { SignupForm } from './SignupForm/SignupForm';
export { PasswordReset } from './PasswordReset/PasswordReset';
// Usage
import { LoginForm, SignupForm } from '@/features/auth/components';
Example: Tree-shaking friendly exports
// ❌ Bad - Breaks tree-shaking (imports everything)
// shared/utils/index.ts
export * from './format';
export * from './validation';
export * from './api';
export * from './date';
// Usage imports ALL utils even if only using one
import { formatCurrency } from '@/shared/utils';
// ✅ Good - Named exports (better tree-shaking)
// shared/utils/index.ts
export { formatCurrency, formatDate } from './format';
export { validateEmail, validatePhone } from './validation';
export { apiClient, handleApiError } from './api';
// ✅ Better - Direct imports (best tree-shaking)
import { formatCurrency } from '@/shared/utils/format';
import { validateEmail } from '@/shared/utils/validation';
// ✅ Best - Namespace exports for related functions
// shared/utils/format/index.ts
export const format = {
currency: (value: number) => `${value.toFixed(2)}`,
date: (date: Date) => date.toLocaleDateString(),
percent: (value: number) => `${(value * 100).toFixed(2)}%`
};
// Usage
import { format } from '@/shared/utils/format';
format.currency(100); // "$100.00"
Example: Component library barrel exports
// shared/components/index.ts - Component library
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
export { Card } from './Card';
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';
// Usage
import { Button, Card, type ButtonProps } from '@/shared/components';
// Alternative: Subpath exports in package.json
// package.json
{
"exports": {
".": "./src/index.ts",
"./button": "./src/Button.tsx",
"./input": "./src/Input.tsx",
"./modal": "./src/Modal.tsx"
}
}
// Usage with subpaths (better performance)
import { Button } from '@repo/ui/button';
import { Input } from '@repo/ui/input';
Best Practice: Use barrel exports for feature boundaries and component libraries. Use direct
imports for utilities and helpers. Configure
sideEffects: false in package.json for better
tree-shaking.
4. Absolute Imports Path Mapping
Configure absolute imports to avoid relative path hell and improve code readability with TypeScript path mapping.
| Configuration | Tool/Framework | Config File | Syntax |
|---|---|---|---|
| TypeScript Paths | All TS projects | tsconfig.json | @/*, @components/* |
| Webpack Aliases | CRA, custom webpack | webpack.config.js | resolve.alias |
| Next.js | Next.js projects | tsconfig.json (auto) | @/* by default |
| Vite | Vite projects | vite.config.ts | resolve.alias |
Example: TypeScript path mapping configuration
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/shared/components/*"],
"@features/*": ["src/features/*"],
"@hooks/*": ["src/shared/hooks/*"],
"@utils/*": ["src/shared/utils/*"],
"@types/*": ["src/shared/types/*"],
"@lib/*": ["src/lib/*"],
"@styles/*": ["src/styles/*"]
}
}
}
// Before (relative imports - hard to read)
import Button from '../../../shared/components/Button';
import { formatDate } from '../../../../shared/utils/format';
import { useAuth } from '../../features/auth/hooks/useAuth';
// After (absolute imports - clean and clear)
import Button from '@components/Button';
import { formatDate } from '@utils/format';
import { useAuth } from '@features/auth/hooks/useAuth';
// Alternative: Single @ prefix
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
// Usage
import Button from '@/shared/components/Button';
import { formatDate } from '@/shared/utils/format';
import { useAuth } from '@/features/auth/hooks/useAuth';
Example: Vite configuration with aliases
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/shared/components'),
'@features': path.resolve(__dirname, './src/features'),
'@hooks': path.resolve(__dirname, './src/shared/hooks'),
'@utils': path.resolve(__dirname, './src/shared/utils'),
'@types': path.resolve(__dirname, './src/shared/types'),
'@styles': path.resolve(__dirname, './src/styles')
}
}
});
// tsconfig.json (must match Vite config)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/shared/components/*"],
"@features/*": ["src/features/*"],
"@hooks/*": ["src/shared/hooks/*"],
"@utils/*": ["src/shared/utils/*"],
"@types/*": ["src/shared/types/*"],
"@styles/*": ["src/styles/*"]
}
}
}
Example: Next.js automatic path mapping
// tsconfig.json - Next.js auto-configures @/* alias
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"]
}
}
}
// Usage in Next.js
// app/page.tsx
import { Button } from '@/components/ui/Button';
import { db } from '@/lib/database';
// Monorepo with multiple apps
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"paths": {
"@web/*": ["./src/*"],
"@repo/ui": ["../../packages/ui/src/index.ts"],
"@repo/utils": ["../../packages/utils/src/index.ts"]
}
}
}
Example: Jest configuration for path mapping
// jest.config.js - Match TypeScript paths
module.exports = {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"^@components/(.*)$": "<rootDir>/src/shared/components/$1",
"^@features/(.*)$": "<rootDir>/src/features/$1",
"^@hooks/(.*)$": "<rootDir>/src/shared/hooks/$1",
"^@utils/(.*)$": "<rootDir>/src/shared/utils/$1"
}
};
// Alternative: Use ts-jest preset (auto-reads tsconfig paths)
module.exports = {
preset: 'ts-jest',
moduleNameMapper: {
"^@/(.*)\\$": "<rootDir>/src/$1"
}
};
Common Pitfalls: Ensure build tools (Webpack, Vite, Jest) configurations match tsconfig.json
paths.
Use relative imports for files in same feature. Don't mix @ and ~ prefixes. Keep path mappings simple (2-3 max).
5. ESLint Prettier Husky Configuration
Enforce code quality and consistency with automated linting, formatting, and pre-commit hooks.
| Tool | Purpose | When It Runs | Config File |
|---|---|---|---|
| ESLint | Find code quality issues, enforce rules | On save, pre-commit, CI | .eslintrc.json |
| Prettier | Format code consistently | On save, pre-commit | .prettierrc |
| Husky | Git hooks automation | Pre-commit, pre-push | .husky/ |
| lint-staged | Run linters on staged files only | Pre-commit | .lintstagedrc |
Example: Complete ESLint + Prettier + Husky setup
// Installation
npm install -D eslint prettier eslint-config-prettier eslint-plugin-prettier
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install -D eslint-plugin-react eslint-plugin-react-hooks
npm install -D husky lint-staged
// Initialize Husky
npx husky init
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier" // Must be last to override other configs
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"],
"rules": {
"prettier/prettier": "error",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-module-boundary-types": "off",
"react/react-in-jsx-scope": "off", // Not needed in React 17+
"react/prop-types": "off", // Using TypeScript
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"settings": {
"react": {
"version": "detect"
}
}
}
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}
// .prettierignore
node_modules
.next
dist
build
coverage
*.min.js
Example: Husky + lint-staged configuration
// package.json
{
"scripts": {
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"prepare": "husky"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
// .husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run type-check
npm run test
// .lintstagedrc.js (alternative config file)
module.exports = {
'*.{ts,tsx}': [
'eslint --fix',
'prettier --write',
() => 'tsc --noEmit' // Type check all files
],
'*.{js,jsx}': ['eslint --fix', 'prettier --write'],
'*.{json,md,css}': ['prettier --write']
};
Example: Next.js ESLint configuration
// .eslintrc.json - Next.js specific
{
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@next/next/no-html-link-for-pages": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}]
}
}
// VS Code settings.json - Auto-format on save
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
Best Practice: Run
prettier through ESLint with eslint-plugin-prettier. Use
lint-staged to only check changed files (faster). Configure editor to format on save. Add
type-check
to pre-push hook.
6. TypeScript Strict Mode Configuration
Enable TypeScript strict mode for maximum type safety and catch errors at compile-time instead of runtime.
| Strict Option | What It Does | Common Issues It Catches |
|---|---|---|
| strict | Enables all strict checks (recommended) | All type-related issues |
| strictNullChecks | null/undefined must be explicit | Null pointer exceptions |
| strictFunctionTypes | Strict function parameter checking | Function type mismatches |
| noImplicitAny | No implicit any types | Missing type annotations |
| noUnusedLocals | Error on unused variables | Dead code, typos |
| noImplicitReturns | All code paths must return | Missing return statements |
Example: Strict TypeScript configuration
// tsconfig.json - Recommended strict settings
{
"compilerOptions": {
// Strict Type Checking
"strict": true, // Enable all strict options
"noImplicitAny": true, // No implicit 'any'
"strictNullChecks": true, // Strict null/undefined checks
"strictFunctionTypes": true, // Strict function types
"strictBindCallApply": true, // Strict bind/call/apply
"strictPropertyInitialization": true, // Class properties must be initialized
"noImplicitThis": true, // No implicit 'this'
"alwaysStrict": true, // Parse in strict mode
// Additional Checks
"noUnusedLocals": true, // Error on unused variables
"noUnusedParameters": true, // Error on unused parameters
"noImplicitReturns": true, // All paths must return
"noFallthroughCasesInSwitch": true, // No fallthrough in switch
"noUncheckedIndexedAccess": true, // Index signatures return T | undefined
"allowUnreachableCode": false, // Error on unreachable code
// Module Resolution
"moduleResolution": "bundler", // Modern resolution
"resolveJsonModule": true, // Import JSON files
"isolatedModules": true, // Each file as separate module
"esModuleInterop": true, // Better CommonJS interop
"skipLibCheck": true, // Skip type checking .d.ts files
// Emit
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx", // React 17+ JSX transform
"module": "ESNext",
"sourceMap": true,
// Path Mapping
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build"]
}
Example: Strict null checks and proper typing
// ❌ Without strictNullChecks
function getUser(id: string) {
const user = database.findUser(id); // Could be undefined
return user.name; // Runtime error if user is undefined
}
// ✅ With strictNullChecks
function getUser(id: string): string | undefined {
const user = database.findUser(id); // User | undefined
// Must handle null/undefined explicitly
if (!user) {
return undefined;
}
return user.name;
}
// ✅ Better - Use optional chaining
function getUserName(id: string): string | undefined {
const user = database.findUser(id);
return user?.name; // Safe navigation
}
// ✅ Best - Use nullish coalescing
function getUserNameOrDefault(id: string): string {
const user = database.findUser(id);
return user?.name ?? 'Anonymous';
}
// ❌ noImplicitAny error
function process(data) { // Error: Parameter 'data' implicitly has 'any' type
return data.value;
}
// ✅ Fixed with explicit type
function process(data: { value: string }): string {
return data.value;
}
// ✅ Generic for reusability
function process<T>(data: { value: T }): T {
return data.value;
}
Example: Class strictPropertyInitialization
// ❌ Without strictPropertyInitialization
class User {
name: string; // No error, but undefined at runtime
email: string;
}
// ✅ Fixed - Initialize in constructor
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
// ✅ Alternative - Definite assignment assertion (use carefully)
class User {
name!: string; // ! tells TS we'll initialize it
email!: string;
async init(id: string) {
const data = await fetchUser(id);
this.name = data.name;
this.email = data.email;
}
}
// ✅ Best - Use optional or union types
class User {
name?: string; // Explicitly optional
email: string | null; // Explicitly nullable
constructor(email: string | null) {
this.email = email;
}
}
// React component with strict mode
interface Props {
user?: User; // Explicitly optional
onSave: (data: User) => void;
}
function UserProfile({ user, onSave }: Props) {
if (!user) {
return <div>No user data</div>;
}
// TypeScript knows user is defined here
return <div>{user.name}</div>;
}
Example: noUncheckedIndexedAccess
// Without noUncheckedIndexedAccess
const colors = ['red', 'green', 'blue'];
const color = colors[5]; // color: string (wrong!)
console.log(color.toUpperCase()); // Runtime error
// ✅ With noUncheckedIndexedAccess enabled
const colors = ['red', 'green', 'blue'];
const color = colors[5]; // color: string | undefined (correct!)
if (color) {
console.log(color.toUpperCase()); // Safe
}
// Dictionary access
interface UserMap {
[id: string]: User;
}
const users: UserMap = { '1': { name: 'John' } };
// Without noUncheckedIndexedAccess
const user = users['999']; // user: User (wrong!)
// With noUncheckedIndexedAccess
const user = users['999']; // user: User | undefined (correct!)
if (user) {
console.log(user.name);
}
Project Structure Best Practices
| Aspect | Recommendation | Rationale |
|---|---|---|
| Monorepo | Nx (enterprise), Turborepo (modern), pnpm (simple) | Choose based on team size and complexity |
| Folder Structure | Feature-based with shared folder | Scalable, clear ownership, easy refactoring |
| Barrel Exports | Use for features, avoid for utilities | Clean API, but watch bundle size |
| Path Mapping | @ prefix with 2-3 aliases max | Clean imports without path hell |
| Code Quality | ESLint + Prettier + Husky + lint-staged | Automated consistency, catch errors early |
| TypeScript | strict: true with all checks enabled | Maximum type safety, fewer runtime errors |