Beyond the Basics: A Curated List of TypeScript Tricks for Better Coding

This is a list of TypeScript types and patterns that I've found useful in everyday coding. I've omitted the well-known ones, so if you're looking for the basics, you might want to check out the TypeScript documentation first.

Prettify

When you union or intersect types, the resulting type can be hard to read. For example:

type User = { name: string };
type Image = { url: string; alt: string };

type UserOrImage = User | Image;
//   ^? UserOrImage = User | Image
type UserWithProfilePicture = User & { profilePicture: Image };
//   ^? type UserWithProfilePicture = User & {
//          profilePicture: Image
//      }

Wouldn't it be nice if TypeScript would automatically expand the types for you?

With Prettify, and its recursive counterpart DeepPrettify, you can do just that.

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

type DeepPrettify<T> = {
  [K in keyof T]: DeepPrettify<T[K]>;
} & {};

Examples:

type UserOrImage = Prettify<User | Image>;
//   ^? {
//          name: string;
//      } | {
//          url: string;
//          alt: string;
//      }
type UserWithProfilePicture = DeepPrettify<User & { profilePicture: Image }>;
//   ^? type UserWithProfilePicture = {
//          name: string;
//          profilePicture: {
//              url: string;
//              alt: string;
//          };
//      }

ExactKeyOf

When you have an object, you can get the keys of that object through the keyof operator. However, when you have Record<string, any>, keyof will return string | number | symbol. Using ExactKeyOf, you get what you expect.

type ExactKeyOf<T> = T extends Record<infer U, any> ? U : keyof T;

Examples:

type StringKey = ExactKeyOf<Record<string, any>>; // string
type NumberKey = ExactKeyOf<Record<number, any>>; // number
type SymbolKey = ExactKeyOf<Record<symbol, any>>; // symbol
type ObjectKey = ExactKeyOf<{ a: 1; b: 2 }>; // 'a' | 'b'
type UnionKey = ExactKeyOf<{ a: 1; b: 2 } | { c: 3; d: 4 }>; // 'a' | 'b' | 'c' | 'd'

Const Assertions

When you have a literal type, you can use as const to assert that the type is a literal type and not a general type.

const x = 'hello';
//    ^? Type: string
const y = 'hello' as const;
//    ^? Type: 'hello'

This might be quite well-known, but I've still seen people not using it. Documentation only exists in the release notes of TypeScript 3.4! So, I'm including it here.

Correlated Union Types

A tagged union is a union where each member has a tag that distinguishes it from the others. This is useful for modeling state machines, for example:

type LoadingState = { state: 'loading' };
type SuccessState<T> = { state: 'success'; data: T };
type ErrorState<T extends Error> = { state: 'error'; error: T };

type Response<T> = Loading | Success<T> | Error<T>;

This allows you to easily switch on the state property to narrow the type:

const response: Response<number> = ...
switch (response.state) {
  case 'loading':
    return 'Loading...';
  case 'success':
    return response.data;
  case 'error':
    return response.error.message;
}

Correlated union types are tagged unions with shared keys between the members, but the types of the shared keys are different. For example:

type Button = { type: 'button'; color: 'red' | 'blue' };
type Link = { type: 'link'; color: 'green' | 'yellow' };

type Element = Button | Link;
function getColor(element: Element) {
  switch (element.type) {
    case 'button':
      return element.color;
    case 'link':
      return element.color;
  }
}
const color = getColor({ type: 'button', color: 'red' });
//    ^? Type: 'red' | 'blue' | 'green' | 'yellow'

In this case, to narrow the type, you need to use generics. For example:

function getColor<T extends Button | Link>(element: T): T['color'] {
  return element.color;
}
const color = getColor({ type: 'button', color: 'red' });
//    ^? Type: 'red'

Similarly, there are times when you want to inform TypeScript that a property cannot exist in a type. For example:

type RenderList<T> =
  | {
      items: string[];
    }
  | {
      items: T[];
      renderItem: (item: T) => string;
    };

In this case, renderItem should only exist when items is an array of T. You can use correlated union types to model this:

type RenderList<T> =
  | {
      items: string[];
      renderItem?: never;
    }
  | {
      items: Exclude<T, string>[];
      renderItem: (item: Exclude<T, string>) => string;
    };

Compared to the previous example, you will get fewer errors because now TypeScript knows that renderItem should only exist when items is not an array of strings. Users can't accidentally add renderItem, allowing better type safety.

Conclusion

These are just some of the types and patterns I've found useful, but TypeScript features grow with each release. Learning these functionalities take time and practice, so don't worry if you don't understand them immediately. Keep coding and experimenting, and you'll get there!