Type the returnType of a function that receive dynamic args

Viewed 49

I have a function that receives args that can be either typed as string or as [string, Record<string, string>].

This function returns an object for which the keys are either the args of the function if these args are typed string or the first string in the array if these arg are typed [string, Record<string, string>]. All the values of this object are typed as string

For example :

exampleFunction("test1", "test2", ["test3", { result: "randomstring" }], "test4");

should return something typed like this:

type tResult = {
  test1: string;
  test2: string;
  test3: string;
  test4: string;
};

This function can take any amount of args and any of those args can be either string or [string, Record<string, string>].

The challenge is to type the returnType of this exampleFunction.

Typescript sandbox link here

I tried to type it tExampleFunctionReturn<T> with:

type tExampleFunctionReturn<T extends tExampleFunctionArgs> = Record<
  T[number] extends string ? T[number] : T[number][0],
  string
>

But it does not work.

Can someone please help me ? Thank you for your answer.

1 Answers

The TypeScript compiler is unable to infer that the output of args.reduce(...) will be a value of ExampleFunctionReturn<T>. The typings for Array.prototype.reduce() only model the situation where the accumulator's type stays the same during the operation. You are incrementally adding properties to an object, but the compiler only sees it as type {}, the type it infers for the initial empty object you pass in.

It is probably hopeless to try to guide the compiler to understand the gradual mutation of type from {} to the final output type. The language would probably need so-called higher-kinded types of the sort requested in microsoft/TypeScript#1213 just to be able to express such a relationship, and who knows if inference would work even then. But without such higher-kinded types there's not even a reasonable place to start.

Instead of trying to get the compiler to infer the type, you will get further if you just compute the type yourself and assert that the value in question is of that type. Here's how I'd approach it:

const exampleFunction = <K extends string>(
  ...args: Array<K | [K, Record<string, string>]>
) => {
  return args.reduce((acc, arg) => {
    if (typeof arg == "string") {
      return {
        ...acc,
        [arg]: "randomstring",
      };
    } else {
      return {
        ...acc,
        [arg[0]]: arg[1].result,
      };
    }
  }, {} as { [P in K]: string }); // assert here
};

Note that I've changed the typings from your example; I am no longer using a generic parameter T corresponding to the type of the args rest parameter. Since all you are trying to track is the literal types of the intended keys, I've instead made the generic type parameter K represent the union of these keys. Then the args array elements are of type K | [K, Record<string, string>].

This is easier than starting with the whole array type T and trying to tease the keys from it.

It also has the advantage that K extends string gives the compiler a hint that you want string literal types. With your original typing, the value ["test3", {...}] will unfortunately be widened to string unless you use a const assertion or something like it.

Anyway, I have asserted that the accumulator is of a type equivalent to Record<K, string>, and therefore the output of exampleFunction() is the same type. Let's see if it works:

const result = exampleFunction(
  "test1", "test2", ["test3", { result: "randomstring" }], "test4"
);
/* const result: {
    test1: string;
    test2: string;
    test3: string;
    test4: string;
} */

console.log(result.test1.toUpperCase()) // RANDOMSTRING

Looks good!

Playground link to code

Related