Search code examples
typescripttypescript-types

TypeScript Template Literal Type - how to infer numeric type?


// from a library
type T = null | "auto" | "text0" | "text1" | "text2" | "text3" | "text4";

//in my code
type N = Extract<T, `text${number}`> extends `text${infer R}` ? R : never

(TS playground)

For the above piece of code N will be equivalent to "0" | "1" | "2" | "3" | "4". How can I convert that to a numeric type, i.e. 0 | 1 | 2 | 3 | 4? Have already tried putting & number in some places, like infer R & number, but none of that works.


Solution

  • 6 June 2022 UPDATE TS 4.8

    Since TypeScript 4.8, it is possible without numeric union hack. See PR

    
    //in my code
    type ParseInt<T extends `text${number}`> =
      T extends any
      ? (T extends `text${infer Digit extends number}`
        ? Digit
        : never)
      : never
    
    // 0 | 1 | 2 | 3 | 4
    type Result = ParseInt<"text0" | "text1" | "text2" | "text3" | "text4">
    

    Playground

    UPDATE

    type MAXIMUM_ALLOWED_BOUNDARY = 999
    
    type Mapped<
        N extends number,
        Result extends Array<unknown> = [],
        > =
        (Result['length'] extends N
            ? Result
            : Mapped<N, [...Result, Result['length']]>
        )
    
    
    type NumberRange = Mapped<MAXIMUM_ALLOWED_BOUNDARY>[number] // 0.. 998
    
    
    type ConvertToNumber<T extends string, Range extends number> =
        (Range extends any
            ? (`${Range}` extends T
                ? Range
                : never)
            : never)
    
    type _ = ConvertToNumber<'5', NumberRange> // 5
    type __ = ConvertToNumber<'125', NumberRange> // 125
    

    Playground

    P.S. sorry for naming, I'm not strong in it.

    Seems to be it is currently impossible but there is a workaround.

    You can create Dictionary for numbers in range 0..42:

    // from a library
    type Texts<T extends PropertyKey> = T extends number ? `text${T}` : never
    
    type T = null | "auto" | Texts<Enumerate<43>>;
    
    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;
    
    type Dictionary = {
        [Prop in Enumerate<43> as `${Prop}`]: Prop
    }
    
    //  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 ... 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42
    type N =
        Extract<T, `text${number}`> extends `text${infer R}`
        ? R extends keyof Dictionary
        ? Dictionary[R]
        : never
        : never
    

    It might be possible to generate much longer range after Tail recursion PR will be merged

    Playground

    UPDATE - just like I promised

    Try

    type MAXIMUM_ALLOWED_BOUNDARY = 999
    
    type Mapped<
        N extends number,
        Result extends Array<unknown> = [],
        > =
        (Result['length'] extends N
            ? Result
            : Mapped<N, [...Result, Result['length']]>
        )
    
    // 0 , 1, 2 ... 998
    type NumberRange = Mapped<MAXIMUM_ALLOWED_BOUNDARY>[number]
    
    
    type Texts<T extends PropertyKey> = T extends number ? `text${T}` : never
    
    
    type T = null | "auto" | Texts<NumberRange>;
    
    type Dictionary = {
        [Prop in NumberRange as `${Prop}`]: Prop
    }
    
    //  0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 ... 998
    type N =
        Extract<T, `text${number}`> extends `text${infer R}`
        ? R extends keyof Dictionary
        ? Dictionary[R]
        : never
        : never
    

    Playground

    You can try above solution in TS playground with TS version 4.5 (nightly) The code is much simpler.

    Here you have javascript representation of Mapped:

    const Mapped = (N: number, Result: number[] = []): number[] => {
        if (Result.length === N) {
            return Result
        }
        return Mapped(N, [...Result, Result.length])
    }
    

    Nothing complicated. Tail recursion.