Home > Software engineering >  How can I iterate over a template literal union type?
How can I iterate over a template literal union type?

Time:10-16

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.

Playground link to code

  • Related