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'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).
/* _____________ 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>,
]
Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
[object Object]Two pieces of syntax, both doing more work than they look like they are doing.
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.
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'.
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.
enum B { a = 'aa' } has a type that is not a union at all, just B.a. Distribution over a one-element union is a no-op, so the result is the bare literal 'aa'.enum D { No, Yes = 'YES' } the type D is D.No | D.Yes, and only D.Yes is string based. Assignability of a union to string requires every member to qualify, so the numeric D.No sinks the entire check and EnumToUnion<D> errors.enum E {} has type never, never satisfies every constraint, and distribution over the empty union yields never back.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges