'Type 'K' cannot be used to index type '{ [key in keyof K]: V; }'.ts(2536)

I want to make a method that returns a new object from my custom Object type.

/*
    type K represent the type of key in an object 
    type V represent the type of value in an object
*/
class Group<K,V> extends Map<K,V> {
   toObject() {
    const result: { [key in keyof K]: V } = {} as { [key in keyof K]: V }
    for (const [key, value] of this.entries()) {
      result[key] = value //Error:Type 'K' cannot be used to index type '{ [key in keyof K]: V; }'.ts(2536) 
    }
    return result
  }
}

key and value have types K and V respectively.

I don't clearly understand the error message it would be good if you explain it to me.

I also tried to make it easier.

class Group<K, V> extends Map<K, V> {
  toObject() {
    const result = {}
    for (const [key, value] of this.entries()) {
      result[key] = value //Error:Type 'K' cannot be used to index type '{}'.ts(2536) 
    }
    return result
  }
}


Solution 1:[1]

The main problem here is that you were using keyof K instead of just K. The keyof type operator gives you the union of the keys (often literal types like "a") of the type it applies to. keyof {a: 1, b: 2, c: 3} is "a" | "b" | "c". So keyof K would be whatever keys you'd expect a value of type K to have. If K is something like string, then keyof K is something like keyof string which is:

type KeyofString = keyof string;
/* type KeyofString = number | typeof Symbol.iterator | "toString" | "charAt" | 
    "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" |
    "match" | "replace" | "search" | "slice" | "split" | "substring" | 
    "toLowerCase" | "toLocaleLowerCase" | "toUpperCase" | "toLocaleUpperCase" | 
    "trim" | "length" | "substr" | "valueOf" | "codePointAt" | "includes" | 
    "endsWith" | "normalize" | "repeat" | "startsWith" | "anchor" | "big" | 
    "blink" | "bold" | "fixed" | "fontcolor" | "fontsize" | "italics" | 
    "link" | "small" | "strike" | "sub" | "sup" | "padStart" | "padEnd" */

Unless you want result to have keys like "toUpperCase" or "trim", then keyof K is not right.

If result[key] makes sense, and if key is of type K, then you want a type you can index into with keys of type K. If you put a value value of type V into that property, then you want a type whose values are of type V. The mapped type {[P in K]: V} is just such a type; the keys are K and the values are V. This sort of mapped type where the value types don't depend on the specific keys is often called a "record" and is used often enough that there's a Record<K, V> utility type provided for this purpose.

Let's use Record<K, V> then:

class Group<K, V> extends Map<K, V> {
  toObject() {
    const result = {} as Record<K, V>; // error!
    // -----------------------> ~
    // Type 'K' does not satisfy the constraint 'string | number | symbol'
    for (const [key, value] of this.entries()) {
      result[key] = value // okay
    }
  }
}

So, that resolves the result[key] = value error, but it introduces a new error. The type Record<K, V> is an error in K, where K does not satisfy a constraint of being a keylike type, string | number | symbol. Note that string | number | symbol is used often enough that there is also a PropertyKey utility type provided to be a short alias for this.

And that really is a problem if your Group<K, V> can really have an arbitrary type for K. A Map object in JavaScript can hold "keys" of any type whatsoever, while a plain object like result can only hold keys of a PropertyKey type. For example, you can use Date objects as "keys" to a Map but not as keys to a plain JavaScript object:

const date = new Date();
const m = new Map<Date, string>();
m.set(date, "hello"); // okay
const o: any = {};
o[date] = "hello"; // error

So if your Group<K, V> really needs a value of type Record<K, V>, then that means you have to constrain K so that the compiler will only let new Group<K, V> exist when K is sufficiently keylike:

class Group<K extends PropertyKey, V> extends Map<K, V> {
  toObject() {
    const result = {} as Record<K, V>; // okay
    for (const [key, value] of this.entries()) {
      result[key] = value // okay
    }
  }
}

Now everything compiles as desired.

Playground link to code

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jcalz