Stefan
Stefan

Reputation: 234

How can I check the value is of type in typescript?

Having something like this

type Size = 'big' | 'small';

How can I test that 'test' if a value is Size

const test = 'equal';
if(test in Size){
    //no luck, 'Size' only refers to a type, but is being used as a value here
}

Can I get the type Size transformed to array keys or an enum somehow?

Is my only option to refactor the type to an enum?

Thank you

Upvotes: 3

Views: 1119

Answers (1)

Aluan Haddad
Aluan Haddad

Reputation: 31803

In TypeScript, all types are erased. They are not available at runtime.

However, since the type in question, 'big' | 'small', is based on values, the way to go here is to start from values and use them to generate type information as needed.

First we create an array of valid size strings

// inferred type: readonly ['big', 'small']
const sizes = ['big', 'small'] as const;

The as const type assertion above causes the precise type of each array element to be inferred thus enabling us to write

// big' | 'small'
export type Size = typeof sizes[number]; 

Which computes the union type of all possible array elements, in other words, Size is the type 'big' | 'small'.

We will use Array.prototype.includes to perform the runtime check.

Full example:

export const sizes = ['big', 'small'] as const;
export type Size = typeof sizes[number];

export function isSize(value: string): value is Size {
  return sizes.includes(value as any); // unfortunate
}

const test = 'equal';
if (isSize(test)) {
  console.log(true);
}

Playground Link

An alternative solution, which obviates not just the need for the as any type assertion in the guard but also the guard itself, is to augment the ReadonlyArray<T> interface provided by TypeScript by add an additional overload signature for includes.

export const sizes: readonly ['big', 'small'] = ['big', 'small'] as const;
export type Size = typeof sizes[number];

declare const test: string;

if (sizes.includes(test)) {
  console.log(test);
}

declare global {
  interface ReadonlyArray<T> {
    includes<T extends U, U>(
      this: readonly T[],
      searchElement: U,
      fromIndex?: number
    ): searchElement is T;
  }
}

Playground Link

If you find both of these options distasteful, you can generalize isSize to an isOneOf (name it as you like) and delegate to it, which obviates the type assertion and the type augmentation by passing an additional parameter.

export const sizes = ['big', 'small'] as const;
export type Size = typeof sizes[number];

export function isOneOf<T extends readonly U[], U>(
  values: T,
  value: U
): value is T[number] {
  return values.includes(value);
}

export function isSize(value: string): value is Size {
  return isOneOf(sizes, value);
}

let test = 'equal';
if (isSize(test)) {
  var x = test;
  console.log(true);
}

Playground Link

Upvotes: 4

Related Questions