Taken from template literals PR
Any one of the types any, string, number, boolean, or bigint in a placeholder causes the template literal to resolve to type string.
This code works as expected:
type X<T> = T extends `test.${infer A}` ? A : never;
type Z = X<`test.${number}`>; // string
type Z1 = X<`test.${bigint}`>; // string
type Z2 = X<`test.${boolean}`>; // "false" | "true"
However, this code works as you expect:
type X<T> = T extends `${infer B}.${infer A}` ? B : never;
type Z = X<`test.${boolean}`>; // "test"
Why it works with boolean
? Because boolean is a union type "true | false" under the hood.
So, this should work:
type Z = X<`test.${1|2|3|4|5}`>; // test
If we already know that it works with union types, I think we can generate some union of numbers.
Taken from my blog, answer:
type Values<T> = T[keyof T];
type LiteralDigits = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type NumberString<T extends number> = `${T}`
type AppendDigit<T extends number | string> = `${T}${LiteralDigits}`
type MakeSet<T extends number> = {
[P in T]: AppendDigit<P>;
};
type RemoveTrailingZero<
T extends string
> = T extends `${infer Fst}${infer Rest}`
? Fst extends `0`
? RemoveTrailingZero<Rest>
: `${Fst}${Rest}`
: never;
type From_1_to_999 = RemoveTrailingZero<
Values<
{
[P in Values<MakeSet<LiteralDigits>>]: AppendDigit<P>;
}
>
>;
type By<V extends NumberString<number>> = RemoveTrailingZero<
Values<
{
[P in V]: AppendDigit<P>;
}
>
>;
/**
* Did not use recursion here,
* because my CPU will blow up
*/
type From_1_to_99999 =
| From_1_to_999
| By<From_1_to_999>
| By<From_1_to_999 | By<From_1_to_999>>;
type X<T extends string> = T extends `${infer B}.${infer A}` ? B : never
type Z = X<`test.${From_1_to_99999}`>; // test
Playground
There is another one solution, not so expensive for CPU, but with own limitations:
type PrependNextNum<A extends Array<unknown>> = A["length"] extends infer T
? ((t: T, ...a: A) => void) extends (...x: infer X) => void
? X
: never
: never;
type EnumerateInternal<A extends Array<unknown>, N extends number> = {
0: A;
1: EnumerateInternal<PrependNextNum<A>, N>;
}[N extends A["length"] ? 0 : 1];
type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[]
? E
: never;
// Up to 42 - meaning of the life
type Result = Enumerate<43>; // 0 | 1 | 2 | ... | 42
type X<T extends string> = T extends `${infer B}.${infer A}` ? B : never
type Z = X<`test.${Result}`>; // test
Playground
SO it is up to you which solution is better for you.