TypeScript Best Practices: Beyond the Basics
Disclosure: This article may contain affiliate links. We only recommend products we believe in.
Stop Using any
Every any is a hole in your type system. It disables all type checking for that value and everything it touches. Use unknown instead when you genuinely do not know the type, and narrow it with type guards.
// Bad
function processData(data: any) {
return data.name.toUpperCase(); // No error, will crash at runtime if data has no name
}
// Good
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
return (data as { name: string }).name.toUpperCase();
}
throw new Error('Invalid data format');
}
Discriminated Unions for State
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
function renderUsers(state: RequestState) {
switch (state.status) {
case 'idle': return 'Click to load';
case 'loading': return 'Loading...';
case 'success': return state.data.map(u => u.name); // TypeScript knows data exists
case 'error': return `Error: ${state.error}`; // TypeScript knows error exists
}
}
Const Assertions
// Without const assertion: type is string[]
const colors = ['red', 'green', 'blue'];
// With const assertion: type is readonly ['red', 'green', 'blue']
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'
Utility Types You Should Know
Partial<T>: All properties optional (useful for update operations)Required<T>: All properties requiredPick<T, K>: Select specific propertiesOmit<T, K>: Exclude specific propertiesRecord<K, V>: Object with specific key and value typesReturnType<T>: Extract function return type
Zod for Runtime Validation
TypeScript types disappear at runtime. For API boundaries, user input, and external data, use Zod to validate at runtime AND infer TypeScript types:
For more on this topic, see our guide on API Rate Limiting: Implementation Strategies and Best Practices.
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive(),
});
type User = z.infer<typeof UserSchema>; // TypeScript type derived from schema
const result = UserSchema.safeParse(apiResponse);
if (result.success) {
// result.data is fully typed as User
}