Hank-Roughknuckles
Hank-Roughknuckles

Reputation: 578

How to ensure a Typescript string enum has the same key and value

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?

Upvotes: 5

Views: 4733

Answers (5)

Daltron
Daltron

Reputation: 1866

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) ...

Upvotes: 0

Drazen Bjelovuk
Drazen Bjelovuk

Reputation: 5492

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"'.

Upvotes: 2

jcalz
jcalz

Reputation: 329773

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

Upvotes: 6

Ruan Mendes
Ruan Mendes

Reputation: 92314

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.

Upvotes: 1

Matěj Pokorn&#253;
Matěj Pokorn&#253;

Reputation: 17895

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

Upvotes: 0

Related Questions