Search code examples
typescripttypescript-generics

Using typescript, is it possible to infer object keys from function argument values?


Take this string parsing function as an example.

  
  const { a, b } = parseString({
    string, 
    mapping: [{
      param: 'a',
      ...
    },{
      param: 'b',
      ...
    }]

Can i make typescript be aware that the only available keys should be 'a' and 'b'?

These are the types that I currently have.

interface ParseStringArgs {
    string: string;
    mapping: {
        start: string | null;
        end: string | null
        param: string;
    }[];
}

And my attempt at making it read the param key value. Which obviously does not work now as it is (correctly) reading param: string; from the ParseStringArgs interface above.

export function parseString({ string, mapping }) {

...

    type ParamKey = typeof mapping[number]['param'];
    const result: Record<ParamKey, string> = {};

    ... some logic to populate result

    return result

Now the result of the parseString is Record<string, string>

Is it possible to make the return type of the function Record<'a' | 'b', string>?


Solution

  • If you want a function's return type to depend in a general way on the types of its parameters, then you need to make the function generic. You need to do this explicitly by declaring type parameters on the call signature; they aren't inferred for you automatically (although there is a longstanding open feature request at microsoft/TypeScript#17428 for such functionality).

    One way to do that looks like:

    function parseString<const P extends ParseStringArgs>({ string, mapping }: P) {
      type ParamKey = P["mapping"][number]['param'];
      const result = {} as Record<ParamKey, string>;
      return result
    }
    

    Here I've declared a type parameter P constrained to ParseStringArgs and made the function parameter of type P. Note that it's a const type parameter so that when callers use it, the compiler will tend to infer literal types for your inputs, which is what you want so that the "b" value in your object literal is given the type "b" and not string (which would defeat the whole purpose).

    Then the ParamKey type is computed from P via repeated indexed access. You don't want to use typeof on mapping since the compiler will helpfully widen that to something non-generic.

    Oh and the result initializer needs to be asserted as Record<ParamKey, string> because {} is almost certainly not of that type.


    Okay, let's test it:

    const { a, b, z } = parseString({
      // -------> ~ error on z as expected
      string: "xyz",
      mapping: [{
        param: 'a',
      }, {
        param: 'b',
      }]
    });
    

    Looks good. The return type of the function is equivalent to {a: string, b: string} and therefore trying to use destructuring assignment on z is an error, as desired.

    Playground link to code