I have an array of objects, which have a common key "foo" and I am trying to create a custom type out of this property. I am not looking for its value that I can get from a reduce but more its Type. Here is a link to the TypeScript playground with a reproduced example and the expected output Type I need to retrieve:
class Object1 {
static foo = { reducer_one: { a: 1 } }
object_1_method() { }
}
class Object2 {
static foo = { reducer_two: { b: 2 } }
object_2_method() { }
}
class Object3 {
static foo = { reducer_three: { c: 3 } }
object_3_method() { }
}
const myArray = [Object1, Object2, Object3 ];
const reducers = myArray.reduce((acc, cur) => {
return {
...acc,
...cur.foo
}
}, {});
const myArray = [Object1, Object2, Object3 ];
I am trying to get a reduced object type gathering all the different foo properties of all the initial objects in the array.
const finalObject = {
reducer_one: {...},
reducer_two: {...},
reducer_three: {...}
}
I tried to reduce the array to an object with the objects' respective foo property in order to retrieve its type but the output type of the reduce remains an empty object.
Thanks in advance for your help. Hope the description of the issue is clear enough
CodePudding user response:
const finalObject = myArray.reduce((obj, clazz) => {
Object.keys(clazz.foo).forEach(prop => {
obj[prop] = clazz.foo[prop];
});
return obj;
}, {});
CodePudding user response:
You will pretty much need to use a type assertion to tell the compiler what type reducers
is going to be. Let's call this type ReducedType
with the understanding that we will have to define it at some point.
If you use Array.reduce()
or any other method to walk through an array and add properties gradually, the compiler can't really follow what's happening. See
How can i move away from a Partial<T> to T without casting in Typescript for a similar issue.
You could try to use the Object.assign()
function to do this all at once:
const maybeReducers =
Object.assign({}, ...myArray.map(cur => cur.foo)) // any
but the TypeScript library typings for Object.assign()
operating on a variadic number of inputs just returns a value of the any
type, which doesn't help you either.
Let's stick with reduce()
:
const reducers = myArray.reduce((acc, cur) => {
return {
...acc,
...cur.foo
}
}, {} as ReducedType);
So we're asserting that the initial {}
value passed in is ReducedType
. This assertion is sort of a lie that eventually becomes true.
So how do we compute ReducedType
from the type of myArray
? Let's look at myArray
with [the TypeScript typeof
operator]:
type MyArray = typeof myArray
// type MyArray = (typeof Object1 | typeof Object2 | typeof Object3)[]
First we want to get the type of the elements of that array. Since you get elements from an array by indexing into them with a numeric index, we can take the MyArray
type and index into it with the number
type:
type MyArrayElement = MyArray[number]
// type MyArrayElement = typeof Object1 | typeof Object2 | typeof Object3
That's a union of the three class constructor types. From there we want to get the type of each union member's foo
property. We can do that with another indexed access (which distributes across unions):
type MyArrayElementFooProp = MyArrayElement["foo"]
/* type MyArrayElementFooProp = {
reducer_three: {
c: number;
};
} | {
reducer_one: {
a: number;
};
} | {
reducer_two: {
b: number;
};
} */
So that is looking closer to what you want... except it's a union and you really want the intersection of them, since your ReducedType
should have all of those properties.
(And no, I don't know why the compiler chooses to order the union with three
at the front. It doesn't affect type safety at any rate.)
There is a way to transform a union into an intersection using TypeScript's distributive conditional types and conditional type inference, shown here without additional explanation (see the other q/a pair)
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type IntersectionOfFoos = UnionToIntersection<MyArrayElementFooProp>
/* type IntersectionOfFoos = {
reducer_three: {
c: number;
};
} & {
reducer_one: {
a: number;
};
} & {
reducer_two: {
b: number;
};
} */
This is exactly the type we want, although it isn't represented too pleasantly. A type like {x: 0} & {y: 1}
is equivalent to but less pleasing than {x: 0; y: 1}
. We can use a simple mapped type to convert from the former form to the latter:
type ReducedType =
{ [K in keyof IntersectionOfFoos]: IntersectionOfFoos[K] }
/* type ReducedType = {
reducer_three: {
c: number;
};
reducer_one: {
a: number;
};
reducer_two: {
b: number;
};
} */
And we can stop there if you want. It is of course possible to put this together in a single type alias, although it's less obvious what we're doing:
type ReducedType =
typeof myArray[number]["foo"] extends infer F ?
(F extends unknown ? (x: F) => void : never) extends
(x: infer I) => void ?
{ [K in keyof I]: I[K] }
: never : never
/* type ReducedType = {
reducer_three: {
c: number;
};
reducer_one: {
a: number;
};
reducer_two: {
b: number;
};
} */
So there you go, now the compiler knows the type of reducers
strongly enough to let you index into it:
console.log(reducers.reducer_one.a.toFixed(2)); // 1.00
console.log(reducers.reducer_two.b.toFixed(3)); // 2.000
console.log(reducers.reducer_three.c.toFixed(4)); // 3.0000