Search code examples
typescripttype-mapping

Typescript: Type-mapped type cannot be indexed with original type


I am writing a class that is instantiated that has a method whose return type is an object with the keys from the generic type inferred by the compiler. I am running into issues with the following code snippet as the compiler tells me that the generic type U cannot be indexed by type T -- which I believe is incorrect due to the fact that U is type-mapped using T, therefore T will always be able to index an object of type U. Is there a way of affirming to the compiler that this is the case?

Minimal snippet:

class DynamicReturnFormat<T extends string, U = { [ key in T ]: string }> {
  constructor(private keys: T[]) {}

  public returnObject(): U {
    const obj = {} as U;

    for (const key of this.keys) {
      obj[key] = "test";
      // error TS2536: Type 'T' cannot be used to index type 'U'.
    }

    return obj;
  }
}

As per Rich N.'s answer, this class will be used as follows:

declare const input: Buffer;

const fmt = ["length", "packetID", ...];
const handshakeParser = new DataParser(fmt);

const decodedObject = handshakeParser.decode(input);

For context: DynamicReturnFormat is DataParser and returnObject is decode. I would like decodedObject to have a non-"one size fits all" type that only includes the properties that were passed into the class instantiation.


Solution

  • It's a little unclear how you want this to be used, but I'm guessing you want to do something like:

    const keys = ["a", "b", "c"];
    const drf = new DynamicReturnFormat(keys);
    const result = drf.returnObject();
    

    Then result should be {a: "test", b: "test", c: "test" }?

    If so, the code below works. I've had to take out the second generic variable U, and use a Partial as {} is not assignable to type {[key in T]: string}:

    class DynamicReturnFormat<T extends string> {
        constructor(private keys: T[]) { }
    
        public returnObject(): { [key in T]: string } {
            const obj: Partial<{ [key in T]: string }>= {};
    
            for (const key of this.keys) {
                obj[key] = "test";
            }
    
            return obj as { [key in T]: string };
        }
    }
    

    The code above works, but the generic T doesn't add much value I think. If you're hoping to somehow constrain the array passed into the constructor I don't think that can be done this way (?). So the code below may be better:

    class DynamicReturnFormat {
        constructor(private keys: string[]) { }
    
        public returnObject(): { [key: string]: string } {
            const obj: { [key: string]: string } = {};
    
            for (const key of this.keys) {
                obj[key] = "test";
            }
    
            return obj;
        }
    }