bangden.id/blog/typescript-best-practices
EngineeringFeb 20, 20243 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.

TypeScript Best Practices for 2024
BD

Bang Den

Frontend Engineer & Designer

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 };
4
5function 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}
13
14// Usage - TypeScript narrows the type automatically
15const result = fetchUser('123');
16if (result.success) {
17 console.log(result.data.name); // TypeScript knows data exists
18} else {
19 console.log(result.error.message); // TypeScript knows error exists
20}

Template Literal Types

Create precise string types:

TYPESCRIPT
1type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
2type Endpoint = '/users' | '/posts' | '/comments';
3type Route = `${HTTPMethod} ${Endpoint}`;
4
5// 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}
6
7type UpdateUserDTO = Partial<User>; // All optional
8type 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'>;
2
3const 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}
4
5const user = { name: 'John', age: 30 };
6getProperty(user, 'name'); // ✅ Returns string
7getProperty(user, 'age'); // ✅ Returns number
8getProperty(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 };
3
4function createUserId(id: string): UserId {
5 return id as UserId;
6}
7
8function getUser(id: UserId) { /* ... */ }
9
10const userId = createUserId('123');
11const postId = '456' as PostId;
12
13getUser(userId); // ✅ Works
14getUser(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;
5
6// 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.

Share this article

© 2026 Bang Den. Built with and Tailwind.