Search code examples
typescriptmapped-types

How to define properties in a Typescript interface with dynamic elements in the key name?


I have an object that can have an n number of properties, each one the same but with their n value in the name.

Example:

const obj = {
  'data-element-0': 'something',
  'data-element-1': 'something else',
  'data-element-2': 'something as well',
  'data-element-3': 'something to feel included',
};

Is there any way to define this interface with more specificity than just using

interface Obj {
  [key: string]: string;
}

Solution

  • You can do smth like that:

    type Key = `data-element-${1|2|3|4|5|6|7|8|9|0}`
    
    const obj:Record<Key, string> = {
        'data-element-0': 'something',
        'data-element-1': 'something else',
        'data-element-2': 'something as well',
        'data-element-3': 'something to feel included',
        'data-element-yu': 'something to feel included', // error
    };
    
    

    UPDATE I have also make helpers fot double numbers, from: 0-99:

    type NonZeroDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
    
    type NumberHelper = {
      [P in NonZeroDigit]: {
        [Z in NonZeroDigit]: `${P}${Z}`
      }
    }
    
    type NestedValues<T extends Record<string, Record<string, string>>> = {
      [P in keyof T]: P extends string ? Values<T[P]> : never
    }
    type Values<T> = T[keyof T]
    
    type RemoveTrailingZero<T extends string> = T extends `${infer Fst}${infer Snd}` ? Fst extends `0` ? `${Snd}` : `${Fst}${Snd}` : never;
    
    type Numbers_99 = RemoveTrailingZero<Values<NestedValues<NumberHelper>>>
    

    UPDATE

    Here you have an util for generating number range from 0 to 99999

    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>
    }>>
    
    type From_1_to_99999 =
        | From_1_to_999
        | By<From_1_to_999>
        | By<From_1_to_999
            | By<From_1_to_999>>
    
    

    Demo

    UPDATE 3

    If you still want to generate literal numbers, not string numbers, you can use this code, which has been shamelessly stolen from here

    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 Result = Enumerate<43> // 0 | 1 | 2 | ... | 42
    

    *UPDATE 8 September 2021

    Since TS 4.5, see Tail recursion PR, it is possible to generate much longer number range.

    See example:

    type MAXIMUM_ALLOWED_BOUNDARY = 999
    
    type ComputeRange<
        N extends number,
        Result extends Array<unknown> = [],
        > =
        (Result['length'] extends N
            ? Result
            : ComputeRange<N, [...Result, Result['length']]>
        )
    
    const ComputeRange = (N: number, Result: number[] = []): number[] => {
        if (Result.length === N) {
            return Result
        }
        return ComputeRange(N, [...Result, Result.length])
    }
    // 0 , 1, 2 ... 998
    type NumberRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]
    

    Related question

    Playground