4

I want to make a generic type that checks the following on an enum:

  1. all fields are strings
  2. all of the values are equal to their own keys

So in which case, the following enums would be considered "correct":

enum correct1 {
  bar = 'bar',
  baz = 'baz',
}

enum correct2 {
  quux = 'quux',
}

but the following would not:

enum wrongFoo {
  bar = 'bar',
  baz = 'WRONG',
}

enum wrongFoo2 {
  bar = 1
}

What would be the proper syntax to make this happen?

5 Answers5

6

If you're okay with a manual compile-time check (meaning you have to write something manually after your enum definition), you can do this:

type EnsureCorrectEnum<T extends { [K in Exclude<keyof T, number>]: K }> = true;

And then have the compiler evaluate EnsureCorrectEnum<typeof YourEnumObjectHere>. If it compiles, great. If not, there's a problem:

type Correct1Okay = EnsureCorrectEnum<typeof correct1>; // okay
type Correct2Okay = EnsureCorrectEnum<typeof correct2>; // okay

type WrongFooBad = EnsureCorrectEnum<typeof wrongFoo>; // error!
//   ┌─────────────────────────────> ~~~~~~~~~~~~~~~
// Types of property 'baz' are incompatible.

type WrongFoo2Bad = EnsureCorrectEnum<typeof wrongFoo2>; // error!
//   ┌──────────────────────────────> ~~~~~~~~~~~~~~~~
// Types of property 'bar' are incompatible.

The errors are fairly descriptive too.

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Can you explain a bit more how your template works? – Lou Garczynski Jul 01 '20 at 13:19
  • The `T extends { [K in Exclude]: K }` constraint means "`T` must be assignable to an object type where all the non-numeric keys (`K in Exclude`) have values that are the same as the key (`K`)". Does that make sense? – jcalz Jul 01 '20 at 13:51
  • Absolutely! I wasn't sure what the exclude meant, but I guess orcs not needed for pure string enums – Lou Garczynski Jul 02 '20 at 18:13
2

Alternative way that avoids having to declare a new type for every check:

const keyValuesMatch = <T>(kv: { [K in keyof T]: K }) => {};

enum correct {
  bar = 'bar',
  baz = 'baz',
}
enum incorrect {
  bar = 'bar',
  baz = 'wrong',
}

keyValuesMatch(correct);
keyValuesMatch(incorrect); // Type 'incorrect.baz' is not assignable to type '"baz"'.
Drazen Bjelovuk
  • 5,201
  • 5
  • 37
  • 64
1

Sounds like you could use a utility we wrote. It doesn't create an enum per se, but it does create a type-safe object where the keys have their name as their values and use keyof typeof for a bit of string-type safety.

This was created before string enums existed, which is why is called an enum, but isn't really an enum. It's just an object, but you can use it instead of hardcoding strings.

/**
 * This creates a fake string enum like object.  Use like so:
 *     const Ab = strEnum(['a', 'b']);
 *     type AbKeys = keyof typeof Ab;
 * @param keys keys in the enum
 * @returns enum object
 */
export function createStringEnum<T extends string>(keys: T[]): {[K in T]: K} {
    return keys.reduce((res, key) => {
        res[key] = key;
        return res;
    }, Object.create(null));
}

const Ab = createStringEnum(['a', 'b']);
type AbKeys = keyof typeof Ab;

const Bc = createStringEnum(['b', 'c']);
type BcKeys = keyof typeof Ab;

console.log(Bc.blah) // Compilation error blah property does not exist
// Error invalid string
const b: AbKeys = "e"; 
// An enum throws an error, but this isn't really an enum
// Main drawback of this approach
const a: AbKeys = Bc.b;

Even if it does not fit your needs, this could be helpful to others that aren't required to use enums.

Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217
0

Enums in Typescript are objects, so you can use Object.keys function to get all keys in that enum and check, if they equals to theirs values. Since all keys returned with Object.keys function are string, values must be strings too.

enum correct1 {
    bar = 'bar',
    baz = 'baz',
}

enum correct2 {
    quux = 'quux',
}

enum wrongFoo {
    bar = 'bar',
    baz = 'WRONG',
}

enum wrongFoo2 {
    bar = 1
}

function isEnumValid<T extends {}>(validatedEnum: T) : boolean {
    return Object.keys(validatedEnum).every(k => k === validatedEnum[k]);
}

console.log(isEnumValid(correct1)); // true
console.log(isEnumValid(correct2)); // true
console.log(isEnumValid(wrongFoo)); // false
console.log(isEnumValid(wrongFoo2)); // false
Matěj Pokorný
  • 16,977
  • 5
  • 39
  • 48
0

So, I'll add another way I found on the ol'interwebs

Different Overflow

export enum SNIPPET_TYPES {
  WIKIPEDIA = <any>"wikipedia",
  GUTENBERG = <any>"gutenberg"
}

this works

  function(type: SNIPPET_TYPES) {

  ...

  type: SNIPPET_TYPES.GUTENBERG,

  ... (or)

  if (type === SNIPPET_TYPES.GUTENBERG) ...
Daltron
  • 1,729
  • 3
  • 20
  • 37