typescriptgenericsgeneric-constraintskeyof

TypeScript - typesave mapping types of restricted tuple generic


In TypeScript this is not compiling:

export interface Generic<T extends string> {

}

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T]: Generic<T[P]> }
}

Particularly, the Generic<T[P]> fails with Type 'T[P]' does not satisfy the constraint 'string'.. However, since T extends string[], it can be certain, that T[P] extends string for any P in keyof T.

What am I doing wrong here?


I know I can fix the problem with a conditional type:

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T]: T[P] extends string ? Generic<T[P]> : never }
}

But I don't see, why this should be necessary.


Solution

  • If you look at the full error, the third line has a big clue:

    Type 'T[P]' does not satisfy the constraint 'string'.
      Type 'T[keyof T]' is not assignable to type 'string'.
        Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string'.
          Type 'T[string]' is not assignable to type 'string'.(2344)
    

    The problem is that keyof any array type (or tuple type) will be more like string | number | symbol. And an array also has more than it's member type on those keys. For instance:

    // (...items: string[]) => number
    type PushFunction = string[]['push']
    

    See this snippet. There's a lot more than just numbers in array keys:

    // number | "0" | "1" | "2" | "length" | "toString"
    // | "toLocaleString" | "pop" | "push" | "concat"
    // | "join" | "reverse" | "shift" | "slice" | "sort"
    // | "splice" | "unshift" | "indexOf"
    // | ... 15 more ... | "includes"
    type ArrayKeys = keyof [1,2,3]
    

    And Generic<T> requires T to be a string, but, as shown, not all values of all keys of an array are strings.

    Playground


    You can fix the mapped type very simply by intersecting the array keys with number, informing typescript that you only care about the number keys (which are the array indices):

    export interface Generic<T extends string> {
    
    }
    
    export interface Class<T extends string[]> {
      readonly prop: { [P in keyof T & number]: Generic<T[P]> }
    }
    

    Playground