最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Discriminatable enum - Stack Overflow

programmeradmin1浏览0评论

I need to be able to determine whether a value passed as any is an enum. This seems to be impossible with the enum type in Typescript, so ok, I guess I need to make a wrapper type. Ideally, I want to set up something I can use in a type definition. The question is what’s the most ergonomic/idiomatic way to do this? It would be nice to be able to do something like

type Foo = MyEnum(FOO, BAR)

and get something that behaves like

enum {FOO="FOO", BAR="BAR"}

except I can tell that it’s a MyEnum, but I’m guessing this isn’t exactly possible.

The objective is to be able to do identify the type at runtime, so I can have

if (value instanceof Map) {
  ...
}
else if (value instanceof Array) {
  ...
}
else if (<something to identify an enum>) {
  ...
}

I need to be able to determine whether a value passed as any is an enum. This seems to be impossible with the enum type in Typescript, so ok, I guess I need to make a wrapper type. Ideally, I want to set up something I can use in a type definition. The question is what’s the most ergonomic/idiomatic way to do this? It would be nice to be able to do something like

type Foo = MyEnum(FOO, BAR)

and get something that behaves like

enum {FOO="FOO", BAR="BAR"}

except I can tell that it’s a MyEnum, but I’m guessing this isn’t exactly possible.

The objective is to be able to do identify the type at runtime, so I can have

if (value instanceof Map) {
  ...
}
else if (value instanceof Array) {
  ...
}
else if (<something to identify an enum>) {
  ...
}
Share Improve this question edited Feb 5 at 14:54 Don Hosek asked Feb 5 at 14:18 Don HosekDon Hosek 1,0337 silver badges25 bronze badges 5
  • Yes, determine at runtime – Don Hosek Commented Feb 5 at 14:51
  • 1 There are all kinds of ways you could decide to wrap values. This playground link shows one possible approach. Does that fully address the question? If so I'll write up an answer; if not, what am I missing? – jcalz Commented Feb 5 at 15:06
  • That’s pretty close. Is there any way to be able to retain a distinction between the types of two different MyEnums? – Don Hosek Commented Feb 5 at 15:25
  • What's "two different MyEnums" mean here? The example I show distinguishes between a non-"enum" and an "enum", between a MyEnum and SomeOtherEnum, and between MyEnum.FOO and MyEnum.BAR. What specific thing has not been demonstrated? – jcalz Commented Feb 5 at 15:36
  • Never mind, I didn’t look closely enough. Yes, this is the thing. – Don Hosek Commented Feb 5 at 15:39
Add a comment  | 

1 Answer 1

Reset to default 1

Since TypeScript enum values are just strings (or numbers) at runtime, there's no way to tell the difference between MyEnum.FOO and "FOO" (assuming for this question that you always want the key and the value of an enum to be the same). So yes, you need to replace an enum value with some kind of enum-value-like wrapper object that contains more information. There are many possible approaches, but here's one I'll call a TaggedEnum:

type TaggedEnum<N, K extends string> = { [P in K]: {
   tag: N,
   value: P
} }[K]

Here a TaggedEnum<N, K> is an object with a tag property corresponding to the name of the enum type, and a value property corresponding to the value of the enum type. The above is a distributive object type (as coined in microsoft/TypeScript#47109 which distributes across unions in K, so that TaggedEnum<"MyEnum", "FOO" | "BAR"> is equivalent to TaggedEnum<"MyEnum", "FOO"> | TaggedEnum<"MyEnum", "BAR">.

Of course that's just the enum values, you also need the object that holds these enum values at the corresponding keys:

function makeTaggedEnum<N extends string, K extends string>(
   name: N, ...enums: K[]): { [P in K]: TaggedEnum<N, P> } {
   return Object.fromEntries(enums.map(e => [e, { tag: name, value: e }])) as any
}

This uses Object.fromEntries() to create an object whose keys are the same as the enum keys in enums, and whose values are TaggedEnum objects:

const MyEnum = makeTaggedEnum("MyEnum", "FOO", "BAR");
console.log(MyEnum);
/*  {
  "FOO": {
    "tag": "MyEnum",
    "value": "FOO"
  },
  "BAR": {
    "tag": "MyEnum",
    "value": "BAR"
  }
}  */

And if you need a type corresponding to MyEnum, you could either define it directly with TaggedEnum:

type MyEnum = TaggedEnum<"MyEnum", "FOO" | "BAR">`

or in terms of the type of the MyEnum object:

type MyEnum = typeof MyEnum[keyof typeof MyEnum];

Either way it's equivalent to

/* type MyEnum = {
    tag: "MyEnum";
    value: "FOO";
} | {
    tag: "MyEnum";
    value: "BAR";
} */

Now if you want to determine if something is a TaggedEnum, you can check for the tag and value properties:

function isTaggedEnum<N extends string>(
   value: any, name?: N
): value is TaggedEnum<N, string> {
   return value && typeof value === "object" && "tag" in value &&
      (name === undefined || value.tag === name) &&
      "value" in value && typeof value.value === "string";
}

and note that isTaggedEnum() returns a type predicate to narrow the input to the appropriate tagged enum type. If you call isTaggedEnum(value) without a name property, then value can only be narrowed to TaggedEnum<string, string>. Otherwise, if you call isTaggedEnum(value, name), you can narrow value to TaggedEnum<typeof name, string>.


For real enums you would just check the value with ===, but since you have a wrapper object you need to drill into each one. So instead of if (value === MyEnum.FOO) you have to write if (matchesTaggedEnum(MyEnum.FOO, value)):

function matchesTaggedEnum<N extends string, K extends string>(
   reference: TaggedEnum<N, K>, test: TaggedEnum<N, string>): test is TaggedEnum<N, K> {
   return test.value === reference.value
}

This also returns a type predicate to narrow the test input.


Armed with these, let's see how you can take an arbitrary runtime value of type any and determine what it is:

function doAThing(x: any) {
   if (!isTaggedEnum(x)) return "not a tagged enum";
   if (!isTaggedEnum(x, "MyEnum")) return "not my enum";
   if (!matchesTaggedEnum(x, MyEnum.BAR)) return "not BAR";
   return "is BAR";
}

console.log(doAThing(123)); // not a tagged enum
const SomeOtherEnum = makeTaggedEnum("SomeOtherEnum", "BAR", "BAZ", "QUX");
console.log(doAThing(SomeOtherEnum.BAR)) // not my enum
console.log(doAThing(MyEnum.FOO)); // not BAR
console.log(doAThing(MyEnum.BAR)); // is BAR

Looks good. You can tell if something is a tagged enum or not, if it's from a particular tagged enum, and if its value is the same as another tagged enum of the same tag.

Playground link to code

发布评论

评论列表(0)

  1. 暂无评论