String Literal Type Guard in TypeScript

June 19th 2020 TypeScript

String literal types are a lightweight alternative to string enums. With the introduction of the const assertions in TypeScript 3.4, even type guards can be implemented in a DRY manner.

Imagine the following type:

export interface FilterValue {
  key: FilterKey;
  value: string;
}

Where FilterKey is a string literal type:

export type FilterKey = 'name' | 'surname';

Keeping these in mind, let's implement a function that converts an object with key/value pairs (e.g. from a parsed query string) to a FilterValue array (skipping any unsupported keys):

export const FILTER_KEYS: FilterKey[] = ['name', 'surname'];

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (FILTER_KEYS.includes(key as FilterKey)) {
      filter.push({ key: key as FilterKey, value: queryParams[key] });
    }
  }
  return filter;
}

You can notice two issues with this code:

  • The list of allowed literal values is repeated in the FILTER_KEYS array.
  • Type safety is circumvented by explicitly casting the key variable to FilterKey when instantiating a FilterValue:
filter.push({ key: key as FilterKey, value: queryParams[key] });

The latter issue can be fixed with a type guard:

export function isFilterKey(key: string): key is FilterKey {
  return FILTER_KEYS.includes(key as FilterKey);
}

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (isFilterKey(key)) {
      filter.push({ key, value: queryParams[key] });
    }
  }
  return filter;
}

Now, typecasting isn't needed in the parseFilter function anymore. The compiler trusts the isFilter type guard that the key value is of FilterKey type. As long as it's implemented correctly, an invalid value can't be put into a FilterValue by incorrectly applying an explicit cast.

To avoid repeating the string literals in the FILTER_KEYS array, const assertion can be used:

export const FILTER_KEYS = ['name', 'surname'] as const;
export type FilterKey = typeof FILTER_KEYS[number];

The types FILTER_KEYS and FilterKeys remained identical to before. They are just defined differently.

Here's the full final code:

export const FILTER_KEYS = ['name', 'surname'] as const;
export type FilterKey = typeof FILTER_KEYS[number];

export interface FilterValue {
  key: FilterKey;
  value: string;
}

export function isFilterKey(key: string): key is FilterKey {
  return FILTER_KEYS.includes(key as FilterKey);
}

export function parseFilter(queryParams: {
  [key: string]: string;
}): FilterValue[] {
  const filter: FilterValue[] = [];
  for (const key in queryParams) {
    if (isFilterKey(key)) {
      filter.push({ key, value: queryParams[key] });
    }
  }
  return filter;
}

You can check the code at each step as separate commits in the corresponding repository on GitHub.

With TypeScript features such as const assertions and type guards, string literal types can be enhanced to provide even stronger type safety.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License