TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. It brings compile-time type checking, improved tooling, and better maintainability to JavaScript projects β making it the go-to choice for large-scale web applications.
Why TypeScript?
JavaScript's dynamic nature is powerful but can lead to subtle bugs that only surface at runtime. TypeScript addresses this by introducing a type system that:
- Catches errors at compile time, not in production
- Makes code self-documenting through explicit types
- Powers superior IDE support (autocomplete, refactoring, navigation)
- Enables safer refactoring across large codebases
- Facilitates team collaboration with clearer contracts between modules
π‘ According to a study by Airbnb, TypeScript could have prevented 38% of their production bugs.
Setting Up TypeScript in a New Project
With Next.js (recommended)
| 1 | npx create-next-app@latest my-app --typescript |
| 2 | cd my-app |
| 3 | npm run dev |
In an existing JavaScript project
| 1 | npm install --save-dev typescript @types/node @types/react |
| 2 | npx tsc --init |
Your tsconfig.json should include:
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "target": "ES2020", |
| 4 | "strict": true, |
| 5 | "moduleResolution": "bundler", |
| 6 | "jsx": "preserve", |
| 7 | "paths": { |
| 8 | "@/*": ["./src/*"] |
| 9 | } |
| 10 | } |
| 11 | } |
Core TypeScript Concepts
1. Interfaces and Types
Use interfaces for object shapes and types for unions, primitives, or computed shapes:
| 1 | |
| 2 | interface User { |
| 3 | id: string; |
| 4 | name: string; |
| 5 | email: string; |
| 6 | role: 'admin' | 'editor' | 'viewer'; |
| 7 | createdAt: Date; |
| 8 | } |
| 9 | |
| 10 | |
| 11 | type ApiResponse<T> = |
| 12 | | { status: 'success'; data: T } |
| 13 | | { status: 'error'; message: string }; |
2. Generics
Generics make functions and components reusable across types:
| 1 | async function fetchData<T>(url: string): Promise<ApiResponse<T>> { |
| 2 | const res = await fetch(url); |
| 3 | if (!res.ok) { |
| 4 | return { status: 'error', message: `HTTP ${res.status}` }; |
| 5 | } |
| 6 | const data: T = await res.json(); |
| 7 | return { status: 'success', data }; |
| 8 | } |
| 9 | |
| 10 | |
| 11 | const users = await fetchData<User[]>('/api/users'); |
3. Utility Types
TypeScript ships with powerful built-in utility types:
| 1 | |
| 2 | type UserUpdate = Partial<User>; |
| 3 | |
| 4 | |
| 5 | type UserPreview = Pick<User, 'id' | 'name'>; |
| 6 | |
| 7 | |
| 8 | type CreateUserPayload = Omit<User, 'id' | 'createdAt'>; |
| 9 | |
| 10 | |
| 11 | type RolePermissions = Record<User['role'], string[]>; |
4. Type Guards
Type guards let you narrow types at runtime:
| 1 | function isUser(obj: unknown): obj is User { |
| 2 | return ( |
| 3 | typeof obj === 'object' && |
| 4 | obj !== null && |
| 5 | typeof (obj as User).id === 'string' && |
| 6 | typeof (obj as User).email === 'string' |
| 7 | ); |
| 8 | } |
| 9 | |
| 10 | function handleResponse(data: unknown) { |
| 11 | if (isUser(data)) { |
| 12 | console.log(data.email); |
| 13 | } |
| 14 | } |
Structuring a Scalable TypeScript Application
A well-organised project structure is just as important as type safety:
| 1 | src/ |
| 2 | βββ app/ # Next.js App Router pages |
| 3 | βββ components/ # Reusable UI components |
| 4 | β βββ ui/ # Primitive components (Button, Input, etc.) |
| 5 | β βββ features/ # Feature-specific components |
| 6 | βββ lib/ # Utilities and helpers |
| 7 | βββ hooks/ # Custom React hooks |
| 8 | βββ types/ # Shared type definitions |
| 9 | β βββ api.ts |
| 10 | β βββ models.ts |
| 11 | βββ services/ # API calls and data fetching |
Centralise your types in src/types/ and import them across the project β this ensures a single source of truth.
Practical Patterns
Typed API Service
| 1 | |
| 2 | import type { User, CreateUserPayload } from '@/types/models'; |
| 3 | |
| 4 | const BASE_URL = process.env.NEXT_PUBLIC_API_URL; |
| 5 | |
| 6 | export const userService = { |
| 7 | async getAll(): Promise<User[]> { |
| 8 | const res = await fetch(`${BASE_URL}/users`); |
| 9 | if (!res.ok) throw new Error('Failed to fetch users'); |
| 10 | return res.json(); |
| 11 | }, |
| 12 | |
| 13 | async create(payload: CreateUserPayload): Promise<User> { |
| 14 | const res = await fetch(`${BASE_URL}/users`, { |
| 15 | method: 'POST', |
| 16 | headers: { 'Content-Type': 'application/json' }, |
| 17 | body: JSON.stringify(payload), |
| 18 | }); |
| 19 | if (!res.ok) throw new Error('Failed to create user'); |
| 20 | return res.json(); |
| 21 | }, |
| 22 | }; |
Typed React Component with Props
| 1 | |
| 2 | import type { User } from '@/types/models'; |
| 3 | |
| 4 | interface UserCardProps { |
| 5 | user: User; |
| 6 | onEdit?: (id: string) => void; |
| 7 | compact?: boolean; |
| 8 | } |
| 9 | |
| 10 | export function UserCard({ user, onEdit, compact = false }: UserCardProps) { |
| 11 | return ( |
| 12 | <div className={compact ? 'p-2' : 'p-4'}> |
| 13 | <h3>{user.name}</h3> |
| 14 | <p>{user.email}</p> |
| 15 | {onEdit && ( |
| 16 | <button onClick={() => onEdit(user.id)}>Edit</button> |
| 17 | )} |
| 18 | </div> |
| 19 | ); |
| 20 | } |
TypeScript Best Practices
- β
Enable
"strict": true in your tsconfig β it enables the most valuable checks
- β
Avoid
any β use unknown and narrow with type guards instead
- β
Use
const assertions for immutable data: const roles = ['admin', 'user'] as const
- β
Prefer interfaces for public APIs and types for internal/union types
- β
Colocate types close to where they're used; share only truly global types
- β Don't over-engineer β not everything needs a generic type
- β Don't use type assertions (
as) to silence errors; fix the root cause
Summary
TypeScript transforms JavaScript development by making the impossible β discovering an entire class of bugs before your code runs β completely routine. When structured well, a TypeScript codebase becomes self-documenting, easier to refactor, and far more resilient as your team and product grow.
Start with strict mode, define your data models in a shared types/ directory, and let TypeScript guide you to more intentional, robust code.