#30655•Medium

String Enum to Union

Turn a string enum into the union of its string literal values with a one-line template literal type, and reject numeric or mixed enums at the constraint.

One template literal placeholder turns a string enum into a plain string union.

EnumToUnion<T> takes a string enum and gives back the union of the string values behind its members. Enums are the one part of TypeScript that lives in both the value world and the type world, and that split is what makes this challenge interesting: the answer is short, but only once you know what the type A actually is when A is an enum. The type also has to refuse enums that are not purely string based.

enum A {
  a = 'aa',
  b = 'bb',
  c = 'cc'
}
 
type Result = EnumToUnion<A> // expected to be 'aa' | 'bb' | 'cc'

Challenge Instructions: String Enum to Union

Medium

Transform a string enum to union type.

Example:

enum A {
a = 'aa',
b = 'bb',
c = 'cc'
}
 
type Result = EnumToUnion<A>; // expected to be "aa" | "bb" | "cc"

View on GitHub: https://tsch.js.org/30655

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type EnumToUnion<T> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

enum A {
  a = 'aa',
  b = 'bb',
  c = 'cc',
}

enum B {
  a = 'aa',
}

enum C {
  a,
  b,
}

enum D {
  No,
  Yes = 'YES',
}

type cases = [
  Expect<Equal<EnumToUnion<A>, 'aa' | 'bb' | 'cc'>>,
  Expect<Equal<EnumToUnion<B>, 'aa'>>,
  // @ts-expect-error
  EnumToUnion<C>,
  // @ts-expect-error
  EnumToUnion<D>,
]

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full:

[object Object]

Two pieces of syntax, both doing more work than they look like they are doing.

An enum type is already a union

When you declare enum A { a = 'aa', b = 'bb', c = 'cc' } you get two things with the same name. The value A is an object at runtime. The type A is the union of its three member types:

[object Object]

Each member type such as A.a is a literal enum member type. It behaves like the string literal 'aa' in most places, but it is nominal: A.a is assignable to 'aa', while 'aa' is not assignable to A.a. That nominality is the whole point of enums, and it is also exactly what the challenge asks you to throw away.

This is why the common reflex, T[keyof T], does not apply here. That pattern reads the values off an object type, so it works on typeof A (the runtime object). The generic parameter here is the enum type A, which is a union, not an object.

Widening a member type with a template literal

A template literal type with a single placeholder and nothing around it, `${T}`, converts whatever T is into its string form. For an enum member type, the string form is the plain literal that the member was assigned:

[object Object]

Template literal types distribute over unions, in the same way conditional types do. Every member of the union goes through the placeholder on its own and the results are joined back up:

// `${A.a | A.b | A.c}`  ->  `${A.a}` | `${A.b}` | `${A.c}`
//                       ->  'aa' | 'bb' | 'cc'

That is the entire transformation. The nominal enum wrapper is gone and what remains is an ordinary string literal union, structurally comparable with 'aa' | 'bb' | 'cc'.

The constraint is the second half of the answer

T extends string is not decoration. A template literal placeholder only accepts types assignable to string | number | bigint | boolean | null | undefined, so an unconstrained T would not compile at all inside the backticks. Some constraint is mandatory.

Choosing string rather than the wider set is what makes the two failing test cases fail. A numeric enum member type is assignable to number, not to string, so the constraint rejects it before the body is ever evaluated:

enum C {
  a,
  b,
} // type C = C.a | C.b, both numeric
 
// EnumToUnion<C>  ->  Type 'C' does not satisfy the constraint 'string'

Writing T extends string | number would have compiled the body just fine and quietly returned '0' | '1' for C, which is not what the challenge wants. Narrowing the constraint is how you turn an unwanted input into a compile error instead of a wrong answer.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges