Reputation: 5798
Sorry if the title is confusing, I'm not sure how to phrase it. Basically I want a function that will combine any number of objects based on their properties of key
and value
and return the exact type. It's better to show what I mean:
const objA = { key: 'a', value: 123 };
const objB = { key: 'b', value: 'a string' };
const result = combine(objA, objB);
/*
desired type result:
{
a: number,
b: string,
}
*/
function combine(...args: any[]): any {
return args.reduce((acc, obj) => {
acc[obj.key] = obj.value;
return acc;
}, {});
}
Thanks in advance!
Upvotes: 0
Views: 74
Reputation: 329013
I'd probably leave your implementation signature as-is and add a single overload signature to describe the rather involved type manipulation you're trying to represent:
function combine<KV extends Array<{ key: P; value: any }>, P extends keyof any>(
...args: KV
): { [K in KV[number]["key"]]: Extract<KV[number], { key: K }>["value"] };
function combine(...args: any[]): any {
return args.reduce((acc, obj) => {
acc[obj.key] = obj.value;
return acc;
}, {});
}
So this is a generic function, with two type parameters KV
and P
. P
is basically just a generic key type, and could be done away with and replaced with string | number | symbol
(keyof any
is another way of saying that) except for a wrinkle I'll mention in a little bit. For now, just think of it as "some key type".
KV
is an array of objects with a key
property of some key type, and a value
property of any type at all. This is the type of the args
rest argument.
The return type is where all the fun happens. It is a mapped type whose keys are KV[number]["key"]
. KV[number]
means "what you get when you index into KV
with a number
key", i.e., the elements of the KV
array. And KV[number]["key"]
becomes a union of all the key
properties of those elements. For each key K
, the property value will be Extract<KV[number], {key: K}>["value"]
. Extract<KV[number], {key: K}>
means: take the elements of KV
and Extract
just those whose key
property is K
. And given that, we look up its "value"
property.
Meaning: the return type is an object whose keys are the key
properties from args
elements, and whose value types are the value
properties from args
corresponding to each key.
So let's make sure this works:
const result1 = combine(
{ key: "a", value: 123 },
{ key: "b", value: "a string" }
);
// const result1: {a: number; b: string};
Looks good.
Note that If P
were removed from the signature and replaced with just keyof any
, the above type of result1
would have been just {[k: string]: string | number}
... because the keys "a"
and "b"
would have been widened from the desired string literal types to just string
. Using P
as a separate type parameter is a trick that gives the compiler a hint to keep the key types as string literals when the type of KV
is inferred.
Relatedly, objA
and objB
as you defined them would also result in {[k: string]: string | number}
, because the compiler has no hint that you care about "a"
and "b"
as anything but string
. There are different ways to avoid such widening; one is to use const
assertions, as in:
const objA = { key: "a" as const, value: 123 };
const objB = { key: "b" as const, value: "a string" };
which gives you your desired result in combine()
:
const result = combine(objA, objB);
// const result: {a: number; b: string}
Hope that helps; good luck!
Upvotes: 1
Reputation: 7780
Do you want a static type definition from a dynamic list of key/value pairs? Or do you want an object representation?
If the second option:
function combine(...args: Array<{key: string, value: any}>): Array<string, string> {
return args.reduce((acc, obj) => {
acc[obj.key] = typeof(obj.value);
return acc;
}, {});
}
Upvotes: 0