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:
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
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 .
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.
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.
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 [].
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With