neurosnap
neurosnap

Reputation: 5798

Create an object from other objects with exact properties in the type signature

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

Answers (2)

jcalz
jcalz

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!

Link to code

Upvotes: 1

Soc
Soc

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

Related Questions