18

I can write a non-generic type guard to check if a given string is a member of a string enum like this:

enum MyEnum {
  Thing1 = 'thing one',
  Thing2 = 'thing two',
}

const isMyEnum = (token: any): token is MyEnum => {
  return Object.values(MyEnum).includes(token as MyEnum);
};

Is it possible to make this generic, so that I could re-use the same checking logic for many different string enums?

tuff
  • 4,895
  • 6
  • 26
  • 43

4 Answers4

33

You mean like this?

const isSomeEnum = <T>(e: T) => (token: any): token is T[keyof T] =>
    Object.values(e).includes(token as T[keyof T]);

SoisSomeEnum produces type guard functions from enum objects. The type T[keyof T] means the types of the property values of T.

const isMyEnum = isSomeEnum(MyEnum);
// const isMyEnum: (token: any) => token is MyEnum

When you call isSomeEnum(MyEnum), the type T is inferred as typeof MyEnum, and then T[keyof T] is the property values of that, which is MyEnum.

Hope that helps. Good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Works great, thank you! The callout of `T[key of T]` is really useful - that's what I needed to learn. – tuff Oct 08 '19 at 16:40
  • Is the additional "constructing" function necessary? I didn't notice any issues when writing this as a single function. – superbadcodemonkey Apr 04 '20 at 10:53
  • The question asks how to make such a constructing function, so it’s necessary in that sense. – jcalz Apr 04 '20 at 11:36
  • 1
    @superbadcodemonkey You can easily turn it into a single function, but the reverse is not always possible. the currying proves you don't need to know anything about token when you bind the enum type. jcalz with current stable version of TS I would change 'token:any' to 'token:"unknown'. – user1852503 May 06 '22 at 12:16
  • 2
    suggested improvement to avoid use of any and safely type T: `const isSomeEnum = (e: T) => (token: unknown): token is T[keyof T] => Object.values(e).includes(token as T[keyof T]);` – Jan Franciszek Cieślak Sep 28 '22 at 09:13
9

TS string enums and number enums have very different JS emits.

The accepted answer works for OP's case of string enums.

But someone using number enums may naively think that it will also work for their use case. Be careful.

//number enum here
enum E {
  A,
  B,
  C,
}

const isSomeEnum = <T>(e: T) => (token: any): token is T[keyof T] =>
  (Object as any).values(e).includes(token as T[keyof T]);

console.log(isSomeEnum(E)("A")); //expected false, actual true
console.log(isSomeEnum(E)(0));   //expected true , actual true

function isSomeEnum2<T> (e: T) : (token: unknown) => token is T[keyof T] {
  const keys = Object.keys(e)
    .filter((k) => {
      return !/^\d/.test(k);
    });
  const values = keys.map((k) => {
    return (e as any)[k];
  });
  return (token: unknown): token is T[keyof T] => {
    return values.includes(token);
  };
};

console.log(isSomeEnum2(E)("A")); //expected false, actual false
console.log(isSomeEnum2(E)(0));   //expected true , actual true

Playground

Justin AnyhowStep
  • 1,130
  • 3
  • 12
  • 19
3

A variation on top of @jcalz answer is just a single and plain function that can be directly used:

const isEnumValue = <T extends { [k: string]: string }>(something: any, enumObject: T): something is T[keyof T] =>
    typeof something === 'string' && Object.values(enumObject).includes(something);

As Justin AnyhowStep observed, this function works only on string enums, sot I put the T extends { [k: string]: string } clause. In this way we havethis behaviour:

enum StringEnum {
    A = 'aaa',
    B = 'bbb',
}

enum NumberEnum {
    A,
    B,
}

let a;

if (isEnumValue(a, StringEnum)) {
    if (a === 'SOMETHING') {
        // compiler complains:
        // This condition will always return 'false' since the types 'StringEnum' and '"SOMETHING"' have no overlap.
    }
}
if (isEnumValue(a, NumberEnum)) {
    // compiler complains:
    // Argument of type 'typeof NumberEnum' is not assignable to parameter of type '{ [k: string]: string; }'.
    // Property 'A' is incompatible with index signature.
    // Type 'NumberEnum' is not assignable to type 'string'.
}

Belinde
  • 133
  • 8
1

I didn't find such a package on npm and I thought it would be nice to create one: https://www.npmjs.com/package/is-some-enum. I think it will help someone :)