I need to iterate over a string union type generated via template literals.
type Foo = "Foo" | "Foo2" | "Foo3";
type Bar = "Bar" | "Bar2" | `${Foo}Type`;
I want to iterate over all the values of Bar
at runtime. Typically, this is accomplished via a <const>
assertion like so:
const Bar = ["Bar", "Bar2"] as const;
export type BarType = typeof Bar[number];
Bar.forEach((value) => {console.log(value)});
Attempting this with a template literal doesn't seem to be possible because types aren't available at runtime.
const Bar = ["Bar", "Bar2", `${Foo}Type`] as const;
If this isn't possible, what would be the best way to accomplish this design?
How can I create a type composed from another type that can be used at both runtime and compile time?
CodePudding user response:
You can't create JavaScript values from TypeScript types; the static type system is erased when TypeScript is compiled to JavaScript. So type Foo
and type Bar
will not be available to help you create const Foo
and const Bar
. Your only hope is to make values and then use those to help you create types.
Your Foo
type is easy enough to make:
const Foo = ["Foo", "Foo2", "Foo3"] as const;
type Foo = typeof Foo[number];
The const
assertion allows the compiler to keep track of the literal types of the elements of Foo
, and from there you can get the union of those element types by indexing into Foo
with a number
key.
But Bar
is trickier. Here is the simplest way I can think of to do it:
const Bar = ["Bar", "Bar2", ...Foo.map(t => `${t}Type` as const)] as const;
console.log(Bar);
// const Bar: readonly ["Bar", "Bar2", ...("FooType" | "Foo2Type" | "Foo3Type")[]]
type Bar = typeof Bar[number];
// type Bar = "Bar" | "Bar2" | "FooType" | "Foo2Type" | "Foo3Type"
Here I've used the map()
array method to append "Type"
onto every element of Foo
.
First, note that the callback is t => `${t}Type` as const
with its own const
assertion. That tells the compiler that you'd like it to figure out what the template literal expression `${t}Type`
evaluates to as a template literal type. (This behavior was implemented in microsoft/TypeScript#40707.) Since t
is inferred as type Foo
, then `${t}Type` as const
is inferred as type `${Foo}Type`
, which is what you wanted.
The TypeScript call signature for the array map()
method returns an unordered array type, not a tuple type like the type of Foo
. So the compiler doesn't know that, for example, the second element is "Foo2Type"
. All it knows is that the array is of type Array<`${Foo}Type`>
. If you cared about order, we'd need to do something else. But since you don't, this is sufficient.
Anyway, the const Bar = ["Bar", "Bar2", ...Foo.map(✂) as const] as const
line has another const
assertion, which tells the compiler to keep track of "Bar"
and "Bar2"
. So the type of the Bar
value is inferred as:
// const Bar: readonly ["Bar", "Bar2", ...("FooType" | "Foo2Type" | "Foo3Type")[]]
And so if you ask the compiler what the element type is, by indexing into typeof Bar
with number
, you get:
type Bar = typeof Bar[number];
// type Bar = "Bar" | "Bar2" | "FooType" | "Foo2Type" | "Foo3Type"
as desired.