Web Development

TypeScript Best Practices for 2024

JT
Jahanzaib Tayyab
September 15, 2024
6 min read
TypeScriptJavaScriptBest PracticesWeb Development

Why TypeScript Matters

TypeScript has evolved from a nice-to-have to an essential tool for modern web development. Here are the practices I've adopted in 2024.

1. Strict Mode is Non-Negotiable

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true
  }
}

2. Use satisfies for Type Validation

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
} satisfies Config;

// config.apiUrl is still string, not string | number

3. Discriminated Unions for State

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function render(state: AsyncState<User>) {
  switch (state.status) {
    case 'idle':
      return null;
    case 'loading':
      return <Spinner />;
    case 'success':
      return <UserCard user={state.data} />;
    case 'error':
      return <ErrorMessage error={state.error} />;
  }
}

4. Branded Types for IDs

type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const userId = createUserId('user-123');
getUser(userId);  // OK
getOrder(userId); // Error! Type safety wins

5. Const Assertions

const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  CONTACT: '/contact',
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];
// type Route = '/' | '/about' | '/contact'

6. Template Literal Types

type EventName = `on${Capitalize<string>}`;

function addEventListener(event: EventName, handler: () => void) {
  // ...
}

addEventListener('onClick', () => {}); // OK
addEventListener('click', () => {});   // Error!

7. Utility Types

// Pick only what you need
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties required
type RequiredUser = Required<User>;

// Extract return type
type ApiResponse = Awaited<ReturnType<typeof fetchUser>>;

8. Generic Constraints

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'John', age: 30 };
getProperty(user, 'name');  // OK, returns string
getProperty(user, 'email'); // Error! 'email' not in User

Conclusion

TypeScript is a journey, not a destination. These practices have served me well, but always be open to new patterns as the language evolves.

Share this article
JT

Jahanzaib Tayyab

Full Stack Developer & AI Engineer

Passionate about building scalable applications and exploring the frontiers of AI. Writing about web development, cloud architecture, and lessons learned from shipping software.