Engineering•Feb 20, 2024•3 min read
TypeScript Best Practices for 2024
Level up your TypeScript skills with advanced type patterns, utility types, and real-world examples from production codebases.
BD
Bang Den
Frontend Engineer & Designer
Table of Contents
Beyond Basic Types
TypeScript is more than just adding : string to your variables. Let's explore advanced patterns that will make your code bulletproof.
Discriminated Unions
The most powerful pattern in TypeScript:
TYPESCRIPT
1type Result<T, E = Error> =2 | { success: true; data: T }3 | { success: false; error: E };45function fetchUser(id: string): Result<User> {6 try {7 const user = db.findUser(id);8 return { success: true, data: user };9 } catch (e) {10 return { success: false, error: e as Error };11 }12}1314// Usage - TypeScript narrows the type automatically15const result = fetchUser('123');16if (result.success) {17 console.log(result.data.name); // TypeScript knows data exists18} else {19 console.log(result.error.message); // TypeScript knows error exists20}
Template Literal Types
Create precise string types:
TYPESCRIPT
1type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';2type Endpoint = '/users' | '/posts' | '/comments';3type Route = `${HTTPMethod} ${Endpoint}`;45// Route is now:6// 'GET /users' | 'GET /posts' | 'GET /comments' |7// 'POST /users' | 'POST /posts' | ... etc
Utility Types You Should Know
Partial & Required
TYPESCRIPT
1interface User {2 id: string;3 name: string;4 email: string;5}67type UpdateUserDTO = Partial<User>; // All optional8type CreateUserDTO = Required<User>; // All required
Pick & Omit
TYPESCRIPT
1type UserPreview = Pick<User, 'id' | 'name'>;2type UserWithoutEmail = Omit<User, 'email'>;
Record
TYPESCRIPT
1type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;23const roles: UserRoles = {4 'user-1': 'admin',5 'user-2': 'user',6};
Generic Constraints
Make your generics more precise:
TYPESCRIPT
1function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {2 return obj[key];3}45const user = { name: 'John', age: 30 };6getProperty(user, 'name'); // ✅ Returns string7getProperty(user, 'age'); // ✅ Returns number8getProperty(user, 'foo'); // ❌ Error: 'foo' is not a key of user
Branded Types
Prevent mixing similar types:
TYPESCRIPT
1type UserId = string & { readonly brand: unique symbol };2type PostId = string & { readonly brand: unique symbol };34function createUserId(id: string): UserId {5 return id as UserId;6}78function getUser(id: UserId) { /* ... */ }910const userId = createUserId('123');11const postId = '456' as PostId;1213getUser(userId); // ✅ Works14getUser(postId); // ❌ Error - different branded type
Const Assertions
Lock down object literals:
TYPESCRIPT
1const config = {2 endpoint: 'https://api.example.com',3 timeout: 5000,4} as const;56// config.endpoint is now 'https://api.example.com' (literal)7// not just string
Conclusion
TypeScript's type system is Turing complete—meaning it can express virtually any constraint. The key is knowing which patterns to apply and when.