1

I was thinking the following Y will return test, however, it returns never, anyone has any idea the reason why and potential solution?

type X<T> = T extends `${infer U}.${number}` ? U : never
type Y = X<`test.${number}`>; // expect "test" but got never

Playground link

Bill
  • 17,872
  • 19
  • 83
  • 131

1 Answers1

1

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.