Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript - Get function parameters from key in array of objects

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

  • TypeScript Playground of the problem
like image 653
ajmnz Avatar asked Aug 16 '21 14:08

ajmnz


People also ask

How do you get keys in TypeScript?

How to get the keys of a TypeScript interface? To get the union of the keys of a TypeScript interface, we can use the keyof keyword. interface Person { name: string; age: number; location: string; } type Keys = keyof Person; to create the Keys type by setting it to keyof Person .

Can you pass a function as a parameter in TypeScript?

Similar to JavaScript, to pass a function as a parameter in TypeScript, define a function expecting a parameter that will receive the callback function, then trigger the callback function inside the parent function.

How do you pass a number as a parameter in TypeScript?

Parameters are a mechanism to pass values to functions. Parameters form a part of the function's signature. The parameter values are passed to the function during its invocation. Unless explicitly specified, the number of values passed to a function must match the number of parameters defined.

How to get an object's key by its value in typescript?

Use the find () method to get the key by its value. Both of the examples in the code snippet get an object's key by value. We used the Object.keys method in the first example. The method returns an array of the object's keys. However, note that TypeScript types the return value of the Object.keys () method as string [].

How do you map an array of objects in typescript?

Use the `map()` method to get an array of values from an array of objects in TypeScript, e.g. `const ids = arr.map((obj) => obj.id)`. The `map` method will return a new array containing only the values that were returned from the callback function.

What is type assertion in typescript?

TypeScript is telling us that we can't index the object with any string key, it has to be name, department or country. This is why we used a type assertion to type the return value of the Object.keys () method. Now the key parameter in the find method is a union type of the objects keys, so everything works as expected.

How do I get an object's key by its value?

Use the Object.keys () method to get an array of the object's keys. Type the array to be an array of the object's keys. Use the find () method to get the key by its value. Both of the examples in the code snippet get an object's key by value. We used the Object.keys method in the first example. The method returns an array of the object's keys.


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

like image 150
jcalz Avatar answered Oct 10 '22 10:10

jcalz