The real example is that I want to implement a function that returns an object containing all the attributes in the input array by entering an object array.
function unit<T extends object>(arr: T[]) {
let obj = {};
arr.forEach((val) => {
obj = { ...obj, ...val };
});
return obj;
}
const a = { name: "l" };
const b = { age: 1 };
const person = unit([a, b]);
I want person to be a dynamic typescript value based on input, but I have no idea how to implement it.
CodePudding user response:
TypeScript doesn't model mutation of types in loops, so there's no way the compiler will be able to figure out the resulting type of obj
will be for you. You'll have to tell it what type you expect explicitly.
So, what do we expect? Well, I propose that
const person = unit([a, b]);
should result in person
being the intersection of the types of a
and b
:
// const person: { name: string; } & { age: number; }
Note that this won't really be right if the various elements of arr
have conflicting property types. If you wrote unit([{a: 0}, {a: ""}])
, the resulting value would not really be of type {a: number} & {a: string}
. Spreading overwrites properties; it does not intersect them. Still, this is close enough and is often what the compiler does anyway when you spread generic things together:
function spread<T extends object, U extends object>(t: T, u: U) {
const v = { ...t, ...u };
// const v: T & U
}
So that's our goal: produce an intersection of the types of each element of arr
.
First, you should make the unit()
function generic in the tuple type of arr
, like this:
function unit<T extends object[]>(arr: [...T]) {
(where [...T]
is a variadic tuple type that gives the compiler the hint that we'd like T
to be a tuple type if possible)
Otherwise, if you leave it as just generic in the element types of arr
, then your type argument T
will end up being a union of every element type, which is hard to tease apart into its constituents. For example, examine the difference between person
and notPerson
:
const notPerson = unit([Math.random() < 0.5 ? a : b]);
// const notPerson: { name: string; } | { age: number; }
In both cases, in your version, T
would be inferred as {name: string} | {age: number}
. But person
should be of type {name: string} & {age: number}
while notPerson
should be of type {name: string} | {age: number}
. In order to keep those separate then, we need to care not just about the union of all element types, but each individual element type separately.
Okay, so, armed with T
being a tuple, how do we convert it to an intersection of its element types? Like this:
type TupleToIntersection<T extends any[]> =
{ [I in keyof T]: (x: T[I]) => void }[number] extends
(x: infer I) => void ? I : never;
This uses conditional type inference from a contravariant type position to turn a union into an intersection. The technique is similar to the way Transform union type to intersection type works.
The we can just assert that obj
is of the type TupleToIntersection<T>
:
let obj = {} as TupleToIntersection<T>;
but if we do that, the compiler will forget that obj
is object-like and will get mad when you spread it. So let's remind it by using the Extract<T, U>
utility type:
let obj = {} as Extract<TupleToIntersection<T>, object>;
That will be the same as TupleToIntersection<T>
as long as it is truly objectlike and not primitive.
Okay, let's test it:
const person = unit([a, b]);
// const person: { name: string; } & { age: number; }
const notPerson = unit([Math.random() < 0.5 ? a : b]);
// const notPerson: { name: string; } | { age: number; }
const anotherThing = unit([{ a: 1 }, { b: 2 }, { c: 3 }, { d: true }, { e: "" }]);
// const anotherThing: { a: number; } & { b: number; } & { c: number; } &
// { d: boolean; } & { e: string; }
Looks good!
CodePudding user response:
Is this what you're looking for?
type A = { [key: string]: string | number }
type Arr = A[]
const test3: Arr = [{a: "a", b: 1}]
console.log(test3)