typescripttypescript-generics

Add custom keys to type with a generic


I have the following types and function:

interface Values {
  width: number;
  height: number;
};

interface Opts<T extends string> {
  requiredKeys: (keyof Values | NoInfer<T>)[];
  customParsers: Record<T, (text: string) => unknown>;
}

function example<T extends string>(opts: Partial<Opts<T>>): Partial<Values>;

I am trying to make it so they keys from Opts['customParsers'] also appear on the return type of example, extending Values; for example:

example({
  customParsers: {
    x: text => parseInt(text),
  },
}; // returns { width?: number, height?: number, x?: number }

I am not sure how I'd go about this. I've tried the following which unfortunately didn't work:

type Values<T extends Opts<string>> = {
  width: number;
  height: number;
} & {
  [K in keyof T]: ReturnType<T[K]>;
};

Solution

  • In order to keep track of this you need Opts to maintain a relationship between each key of customParsers and the corresponding return type of the method. That means you will probably want Opts to be generic in an object type T, like this:

    interface Opts<T extends object> {
        requiredKeys: (keyof (Values & T))[];
        customParsers: { [K in keyof T]: (text: string) => T[K] }
    }
    
    declare function example<T extends object>(
        opts: Partial<Opts<T>>
    ): Partial<Values & T>;
    

    Here we can see that customParsers is a mapped type over T, so that each property of customParsers at key K is a method that accepts a string input and produces a T[K] output.

    And requiredKeys is an array of element type keyof (Values & T), where the intersection Values & T is effectively the output type of example(), and so requiredKeys should be some set of keys of that type.

    Note that I removed the use of the NoInfer utility type because it isn't strictly necessary (you will get errors if requiredKeys contains elements not present as keys of customParsers or "width" or "height") and because keeping it there actually prevents inference you need. Maybe it should stay there and someone should file an issue about NoInfer in the TypeScript repo, but that's probably out of scope for this question.

    Let's test it:

    const ret = example({
        requiredKeys: ['x'],
        customParsers: {
            x: text => parseInt(text),
        },
    });
    ret.height?.toFixed(1);
    ret.width?.toFixed(1);
    ret.x?.toFixed(1);
    

    Looks good. The type of T is inferred as {x: number}, and so the output type Partial<Values & {x: number}> behaves as desired.

    Playground link to code