TypeScript Tips and Tricks for Better Type Safety

TypeScript Tips and Tricks for Better Type Safety

TypeScript is more than just adding types to JavaScript. When used effectively, it catches bugs at compile time, improves code documentation, and makes refactoring safer. Here are some advanced tips to take your TypeScript skills to the next level.

Use Discriminated Unions

Create type-safe state machines:

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

function handleResult<T>(result: Result<T>) {
  switch (result.status) {
    case 'loading':
      return 'Loading...';
    case 'error':
      return `Error: ${result.error.message}`;
    case 'success':
      return result.data; // TypeScript knows data exists here
  }
}

Leverage Utility Types

TypeScript includes powerful built-in utility types:

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

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

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

// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Omit specific properties
type UserWithoutId = Omit<User, 'id'>;

// Make properties readonly
type ReadonlyUser = Readonly<User>;

Use Template Literal Types

Create precise string types:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = '/users' | '/posts' | '/comments';
type Endpoint = `${HTTPMethod} ${Route}`;

// Endpoint is now:
// 'GET /users' | 'GET /posts' | 'GET /comments' |
// 'POST /users' | 'POST /posts' | ...

Const Assertions

Lock in literal types:

// Without const assertion
const colors = ['red', 'green', 'blue'];
// type: string[]

// With const assertion
const colors = ['red', 'green', 'blue'] as const;
// type: readonly ["red", "green", "blue"]

type Color = typeof colors[number];
// type: "red" | "green" | "blue"

Type Guards

Create custom type checking functions:

interface Dog {
  bark(): void;
}

interface Cat {
  meow(): void;
}

function isDog(animal: Dog | Cat): animal is Dog {
  return (animal as Dog).bark !== undefined;
}

function handlePet(pet: Dog | Cat) {
  if (isDog(pet)) {
    pet.bark(); // TypeScript knows it's a Dog
  } else {
    pet.meow(); // TypeScript knows it's a Cat
  }
}

Branded Types

Create nominal types in a structural type system:

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

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

function getUser(id: UserId) {
  // ...
}

const userId = createUserId('123');
const postId = '456' as PostId;

getUser(userId); // ✓ OK
getUser(postId); // ✗ Error: Type 'PostId' is not assignable to 'UserId'

Conditional Types

Create types that depend on other types:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// More practical example
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;  // string
type B = Flatten<number>;    // number

Mapped Types

Transform existing types:

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  name: string;
  age: number;
}

type NullableUser = Nullable<User>;
// {
//   name: string | null;
//   age: number | null;
// }

Use satisfies Operator

Ensure a value matches a type without widening:

type Colors = 'red' | 'green' | 'blue';

const colors = {
  primary: 'red',
  secondary: 'green',
} satisfies Record<string, Colors>;

// colors.primary is still 'red', not Colors
const p: 'red' = colors.primary; // ✓ OK

Strict Configuration

Enable strict mode in tsconfig.json:

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

Type-Safe Event Emitters

Create fully typed event systems:

type Events = {
  'user:login': (user: User) => void;
  'user:logout': () => void;
  'post:create': (post: Post) => void;
};

class TypedEmitter {
  on<K extends keyof Events>(event: K, callback: Events[K]) {
    // ...
  }
  
  emit<K extends keyof Events>(
    event: K,
    ...args: Parameters<Events[K]>
  ) {
    // ...
  }
}

Keep Learning

TypeScript is constantly evolving. Stay updated with:

Need TypeScript expertise on your project? Let's talk.