TypeScript has evolved from a niche tool to the default choice for serious JavaScript development. In 2025, the patterns and practices have matured significantly. This guide covers the essential patterns you should adopt for maintainable, type-safe code.
Why strict mode matters
Always enable strict mode in your tsconfig.json. It catches more bugs at compile time and forces better coding habits.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
| Option | What it catches |
|---|---|
strict | Enables all strict checks |
noUncheckedIndexedAccess | Array access might be undefined |
noImplicitReturns | All code paths must return |
noFallthroughCasesInSwitch | Catches missing breaks |
Type layers architecture
Think of your types in layers. This creates clear boundaries and prevents leaky abstractions.
Layer responsibilities
- DTO Layer - Validate and transform external data
- Domain Layer - Core business types
- UI Layer - Component-specific types
Utility types you should master
TypeScript provides powerful utility types. Here’s a reference for the most useful ones:
| Utility | Purpose | Example |
|---|---|---|
Partial<T> | Make all properties optional | Form initial state |
Required<T> | Make all properties required | Validated form data |
Pick<T, K> | Select specific properties | API response subset |
Omit<T, K> | Remove specific properties | Entity without ID |
Record<K, V> | Create object type | Lookup tables |
ReturnType<T> | Extract function return type | Infer from function |
Practical example
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// For creating a new user (no id, no createdAt)
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
// For updating (all fields optional)
type UpdateUserInput = Partial<CreateUserInput>;
// For API response (pick only what UI needs)
type UserSummary = Pick<User, 'id' | 'name'>;
Discriminated unions for state
Use discriminated unions to model states that are mutually exclusive. This eliminates impossible states.
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function renderState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'idle':
return <Placeholder />;
case 'loading':
return <Spinner />;
case 'success':
return <DataView data={state.data} />;
case 'error':
return <ErrorMessage error={state.error} />;
}
}
The TypeScript compiler knows exactly which properties are available in each branch. No runtime checks needed.
Branded types for type safety
Branded types prevent mixing up values that share the same primitive type.
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
function fetchUserPosts(userId: UserId): Promise<Post[]> {
// Implementation
}
const userId = createUserId('user-123');
const postId = createPostId('post-456');
// This compiles
fetchUserPosts(userId);
// This fails! Type 'PostId' is not assignable to type 'UserId'
fetchUserPosts(postId);
Const assertions for literals
Use as const to narrow types to their literal values:
// Without as const
const routes = {
home: '/',
about: '/about',
contact: '/contact',
};
// Type: { home: string, about: string, contact: string }
// With as const
const routes = {
home: '/',
about: '/about',
contact: '/contact',
} as const;
// Type: { readonly home: "/", readonly about: "/about", readonly contact: "/contact" }
type Route = typeof routes[keyof typeof routes];
// Type: "/" | "/about" | "/contact"
Generic constraints for flexibility
Write generic functions with constraints to get the best of both worlds: flexibility and type safety.
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Works with any type that has an id
const users: User[] = [/* ... */];
const posts: Post[] = [/* ... */];
const user = findById(users, 'user-123'); // Type: User | undefined
const post = findById(posts, 'post-456'); // Type: Post | undefined
Type guards for runtime checks
Create type guards when you need runtime type checking:
interface Dog {
type: 'dog';
bark(): void;
}
interface Cat {
type: 'cat';
meow(): void;
}
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return pet.type === 'dog';
}
function handlePet(pet: Pet) {
if (isDog(pet)) {
pet.bark(); // TypeScript knows this is a Dog
} else {
pet.meow(); // TypeScript knows this is a Cat
}
}
Type guards must return boolean and use the pet is Type syntax for TypeScript to narrow the type.
Template literal types
TypeScript 4.1+ supports template literal types for string manipulation at the type level:
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// Type: "onClick" | "onFocus" | "onBlur"
type ApiEndpoint = `/api/${string}`;
function fetchFromApi(endpoint: ApiEndpoint) {
// Implementation
}
fetchFromApi('/api/users'); // OK
fetchFromApi('/users'); // Error!
Error handling patterns
Use Result types instead of throwing errors for expected failures:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJson<T>(json: string): Result<T> {
try {
return { ok: true, value: JSON.parse(json) };
} catch (error) {
return { ok: false, error: error as Error };
}
}
const result = parseJson<User>('{"name": "John"}');
if (result.ok) {
console.log(result.value.name); // Type-safe access
} else {
console.error(result.error.message);
}
Conclusion
TypeScript in 2025 is about more than just adding types to JavaScript. It’s about modeling your domain accurately, eliminating impossible states, and catching bugs before they reach production.
Start with strict mode, adopt discriminated unions for state, and gradually introduce branded types and generics as your codebase grows. The upfront investment pays dividends in maintainability and confidence.