Search code examples
typescript

How can I correctly allow Typescript to infer the return type of a function where the input and output object keys are dynamic


This is a working example.

function doSomething<T extends Record<string, I>, I>({
  inputKey = "input",
  outputKey = "output",
  ...rest
}: {
  inputKey?: string;
  outputKey?: string;
} & T): Record<string, I> {
  // Ensure inputKey exists in the input object to prevent runtime errors
  if (!(inputKey in rest)) {
    throw new Error(`Input key "${inputKey}" not found in input object.`);
  }

  return {
    [outputKey]: rest[inputKey],
  };
}

// Example usage:
const result = doSomething({
  input: "value",
  outputKey: "out",
});
console.log(result);

This doesn't yet infer the type of the result, which is what I'm aiming for.


Solution

  • Wrote up a quick example of how you can do this. You will have to not use ...rest however, as i'd at least have no idea how to infer this well and logically.

    type ObjectKeys = symbol | string | number
    function getKeyOfObject<InObject extends object, InKey extends keyof InObject, OutKey extends ObjectKeys>
      (object: InObject, inKey: InKey, outKey: OutKey)
      : { [K in OutKey]: InObject[InKey] } {
      return {
        [outKey]: object[inKey]
      } as any; // Haven't found a way to make the compiler not complain about the result being incorrect
    }
    
    const a = getKeyOfObject({ a: 1, b: "test" }, "a", "test")
    a.test // number
    

    Explaination:

    1. Object Keys are all valid types which can be used to index an Object
    2. InKey extends keyof InObject - forces the key given as InKey to exist in the given Object.
    3. InObject extends object - less strict Record<x,y>, could be replaced by record if I'm not wrong.
    4. Return type doing the magic.

    I don't know the context of how you want to use this. However, unless really necessary I'd reconsider simplifying the function. Currently you are extracting and mapping to a new Object, I can't imagine many scenarios where this would be truly needed. A function for just extracting (with a possible throw) would be simpler to write. Or you could just use the '?' operator to do checked index access.

    Small note - this function will not work with nested properties. These require quite a lot more work. I've implemented something for that in a little config library I once made: see https://github.com/Aderinom/typedconf/blob/master/src/config.builder.ts get function in line 99