Language

TypeScript Best Practices 2024: Types, Generics, and Architecture Patterns

A
Admin
March 9, 2026 • 6 min read • 1,055 words
9
Overall Scoreout of 10

Rating

9/10

Verdict

TypeScript is non-negotiable for any serious JavaScript project in 2024. Enable strict mode from day one and invest in learning generics — it pays dividends at scale.

Pros

  • Catches bugs at compile time
  • Excellent IDE support and autocomplete
  • Generics enable reusable, type-safe code
  • Gradual adoption path from JavaScript
  • Huge ecosystem and community

Cons

  • Initial setup overhead
  • Complex generics can be hard to read
  • Build step required
  • Type errors can be cryptic for beginners

TypeScript Best Practices 2024: Types, Generics & Architecture Patterns

TypeScript has become the default choice for serious JavaScript development. With over 90% of npm's top packages shipping TypeScript declarations and virtually every major framework providing first-class TS support, the question is no longer whether to use TypeScript but how to use it well. This guide covers battle-tested patterns for large codebases.

Always Enable Strict Mode

The single most impactful TypeScript setting is strict: true in your tsconfig.json. It enables a collection of checks that catch entire classes of bugs:

{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "skipLibCheck": true, "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "jsx": "preserve" } }

Type vs Interface: When to Use Which

Both type and interface can describe object shapes, but they have different strengths:

// Use interface for object shapes that may be extended interface User { id: string; email: string; name: string; } interface AdminUser extends User { permissions: string[]; lastLogin: Date; } // Use type for unions, intersections, and computed types type Status = 'active' | 'inactive' | 'pending'; type ApiResponse = { data: T; error: null } | { data: null; error: string }; type UserWithRole = User & { role: 'admin' | 'user' }; // Type aliases for primitives and utilities type UserId = string; type Timestamp = number; type Nullable = T | null; type Optional = T | undefined;

Mastering Generics

Generics are the cornerstone of reusable TypeScript code. They allow you to write functions and types that work with any type while preserving type safety:

// Basic generic function function first(arr: T[]): T | undefined { return arr[0]; } const num = first([1, 2, 3]); // number | undefined const str = first(['a', 'b']); // string | undefined // Generic with constraints function getProperty(obj: T, key: K): T[K] { return obj[key]; } const user = { id: '1', name: 'Alice', age: 30 }; const name = getProperty(user, 'name'); // string const age = getProperty(user, 'age'); // number // getProperty(user, 'invalid'); // ✗ Compile error! // Generic utility type — repository pattern interface Repository { findById(id: string): Promise; findAll(filter?: Partial): Promise; create(data: Omit): Promise; update(id: string, data: Partial>): Promise; delete(id: string): Promise; }

Built-in Utility Types

TypeScript ships with powerful utility types that transform existing types:

interface Post { id: string; title: string; content: string; publishedAt: Date; authorId: string; } // Partial — all fields optional (useful for update payloads) type UpdatePost = Partial

; // Required — all fields required (opposite of Partial) type RequiredPost = Required

; // Pick — select specific fields type PostPreview = Pick

; // Omit — exclude specific fields type CreatePost = Omit

; // Record — dictionary type type PostsBySlug = Record; // ReturnType — infer function return type async function fetchPost(id: string): Promise

{ /* ... */ } type FetchPostResult = Awaited>; // Post // Parameters — infer function parameter types type FetchPostParams = Parameters; // [string]

Discriminated Unions

Discriminated unions are one of TypeScript's most powerful patterns for modeling state:

// Model async state exhaustively type AsyncState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function renderPost(state: AsyncState

) { switch (state.status) { case 'idle': return

Not started

; case 'loading': return ; case 'success': return

; // TypeScript knows data exists case 'error': return ; } } // API response modeling type Result = | { ok: true; value: T } | { ok: false; error: E }; async function safeJsonFetch(url: string): Promise> { try { const res = await fetch(url); if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) }; const data = await res.json() as T; return { ok: true, value: data }; } catch (e) { return { ok: false, error: e instanceof Error ? e : new Error(String(e)) }; } }

Template Literal Types

Template literal types enable powerful string manipulation at the type level:

type EventName = 'click' | 'focus' | 'blur'; type HandlerName = `on${Capitalize}`; // 'onClick' | 'onFocus' | 'onBlur' type CSSProperty = 'margin' | 'padding' | 'border'; type CSSDirection = 'Top' | 'Right' | 'Bottom' | 'Left'; type CSSLonghand = `${CSSProperty}${CSSDirection}`; // 'marginTop' | 'marginRight' | ... | 'borderLeft' // API route type safety type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; type ApiRoute = `/api/${string}`; type RouteHandler = { [M in HttpMethod]?: (req: Request) => Promise; };

Runtime Validation with Zod

TypeScript only checks types at compile time. For runtime safety (API responses, form inputs), use Zod:

import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), name: z.string().min(1).max(100), age: z.number().int().min(0).max(150).optional(), role: z.enum(['admin', 'user', 'moderator']), createdAt: z.coerce.date(), }); type User = z.infer; // Auto-generate TypeScript type! // Validate API responses async function fetchUser(id: string): Promise { const res = await fetch(`/api/users/${id}`); const raw = await res.json(); return UserSchema.parse(raw); // Throws ZodError if invalid } // Safe parse (no throw) const result = UserSchema.safeParse(rawData); if (result.success) { console.log(result.data); // Fully typed User } else { console.error(result.error.issues); // Detailed validation errors }

Declaration Merging & Module Augmentation

// Extend Express Request with custom properties declare global { namespace Express { interface Request { user?: AuthenticatedUser; sessionId?: string; } } } // Extend third-party types declare module 'next-auth' { interface Session { user: { id: string; email: string; role: 'admin' | 'user'; }; } }

Performance & Build Optimization

For large codebases, TypeScript compilation can become a bottleneck. Use these techniques:

{ "compilerOptions": { "incremental": true, "tsBuildInfoFile": ".tsbuildinfo", "composite": true } }

Use isolatedModules: true to ensure each file can be independently transpiled (required by Babel and SWC). Run type checking separately from your build using tsc --noEmit in CI while using a fast transpiler like SWC for development builds.

Conclusion

TypeScript's type system is extraordinarily powerful — the features covered here (generics, discriminated unions, template literals, utility types, Zod integration) are the building blocks of type-safe, self-documenting code that scales. Enable strict mode, invest in learning the advanced patterns, and your codebase will be dramatically easier to maintain and refactor.

#TypeScript#JavaScript#Web Development#Architecture#Best Practices
TypeScript Best Practices 2024: Types, Generics, and Architecture Patterns | Pulse