TypeScript - Get function parameters from key in array of objects

Viewed 1431

I'm trying to write a function that takes as arguments two keys of a specific object inside an array. The keys are the following:

  • A string used to index a specific object
  • A function (from which I need the types of its parameters)

The array of objects is the following

const obj = [
  {
    name: "number",
    func: (foo: number) => foo,
  },
  {
    name: "string",
    func: (foo: string) => foo,
  },
  {
    name: "array",
    func: (foo: string[]) => foo,
  },
];

And this is my approach

type SingleObject = typeof obj[number];
type Name<T extends SingleObject> = T["name"];
type FuncParams<T extends SingleObject> = Parameters<T["func"]>;

const myFunc = async <T extends SingleObject>(
  name: Name<T>,
  params: FuncParams<T>
) => {
  // Do something
};

I thought that when calling myFunc passing "array" (for example) as the first parameter, it would identify the object in question and correctly assign the parameters of the other key (which in this case should be Parameters<(foo: string[] => foo)>)

This is clearly not the case, as I can do the following without any problems

myFunc("number", ["fo"]); // Should only allow a number as the second parameter

myFunc("string", [34]); // Should only allow a string as the second parameter

myFunc("array", [34]); // Should only allow an array of strings as the second parameter

Reference

1 Answers

By default, the compiler will infer the type of a string-valued property as string and not a string literal type like "number".

/* const obj: ({
    name: string; // oops
    func: (foo: number) => number;
} | {
    name: string; // oops
    func: (foo: string) => string;
} | {
    name: string; // oops
    func: (foo: string[]) => string[];
})[]    
*/

In order for your plan to possibly work, you need to convince the compiler that you care about the actual literal value of the name property of your elements of obj. The easiest way to do this is to use a const assertion on obj's initializer:

const obj = [
  {
    name: "number",
    func: (foo: number) => foo,
  },
  {
    name: "string",
    func: (foo: string) => foo,
  },
  {
    name: "array",
    func: (foo: string[]) => foo,
  },
] as const; // <-- const assertion

And now the type of obj is:

/* const obj: readonly [{
    readonly name: "number";
    readonly func: (foo: number) => number;
}, {
    readonly name: "string";
    readonly func: (foo: string) => string;
}, {
    readonly name: "array";
    readonly func: (foo: string[]) => string[];
}] */

where the name fields are the required types "number", "string", and "array".


Next, the best way to have the compiler infer the type of a generic type parameter is to give the compiler a value of that type from which to infer it. That is, inferring T from a value of type T is straightforward, but inferring T from a value of type T["name"] is not.

So my suggestion is something like this:

/* type NameToParamsMap = {
    number: [foo: number];
    string: [foo: string];
    array: [foo: string[]];
} */

const myFunc = async <K extends keyof NameToParamsMap>(
  name: K,
  params: NameToParamsMap[K]
) => {
  // Do something
};

The type NameToParamsMap is a helper object type whose keys are the name fields from obj elements, and whose properties are the parameter lists from the associated func field. Then myFunc can infer the generic type parameter K from the value of the name parameter passed in. Once K is properly inferred, the compiler can check the params parameter to make sure it matches.


Of course you don't want to write NameToParamsMap by hand; you can make the compiler compute it from typeof obj:

type NameToParamsMap = { 
  [T in typeof obj[number] as T["name"]]: Parameters<T["func"]> }

Here I'm using key remapping to iterate over each element T of typeof obj[number], and using T["name"] and T["func"] to produce the keys and values of the object.


Let's make sure it works:

myFunc("number", [1]); // okay
myFunc("number", ["oops"]) // error
// -------------> ~~~~~~
// Type 'string' is not assignable to type 'number'

myFunc("string", [34]); // error
myFunc("array", [34]); // error

Looks good!

Playground link to code

Related