Narrow types use cases

Narrow types use cases

TypeScript union types represent multiple types at the same time.

It is convenient to group types into unions to explain they share something in common, they have common purpose (think of redux reducer which receives an action object of many different shapes).

For TypeScript there is an awesome technique called type narrowing

Type narrowing - technique that allows a compiler (and developer via IDE) to understand which exact type (shape) is present inside code block.

How to narrow types

Let's say we have union of two other types like below:

type SignInAction = {
  type: 'sign-in';
  email: string;
  password: string;
};

type VerifyEmailAction = {
  type: 'verify-email';
  email: string;
  otp_code: string;
};

type Action = SignInAction | VerifyEmailAction;
union type example
Now, we can narrow type of union via runtime check:
const handleAnyAction = (action: Action) => {
  if (action.type === 'sign-in') {
    // βœ… fully typed
    return action.password;
  }
  if (action.type === 'verify-email') {
    // βœ… fully typed
    return action.otp_code;
  }
};
runtime check to narrow types
Also, we can narrow type in types context only via built-in utility type Extract:
// βœ… structure is same as for VerifyEmailAction
type ActionWithOtpCode = Extract<Action, { otp_code: string }>;
type narrowing via Extract
Also, we create a generic guard which will safely narrow types like that:
const guardForActionType = <T extends Action['type']>(
  action: Action,
  type: T
): action is Extract<Action, { type: typeof type }> => action.type === type;

const handleEmailVerification = (action: Action) => {
  // makes sure that action is of type VerifyEmailAction
  if (guardForActionType(action, 'verify-email')) {
    // βœ… fully typed
    return action.otp_code;
  }
  throw new Error('invalid action provided');
};
type narrowing via generic type guard

Type narrowing via generic guard is especially useful for complex types (shapes) where you need to check multiple fields.