Oof there's a lot of pieces to your question. First, to represent what you're doing, I'd suggest defining a type corresponding to "PoseInfo
with a particular PoseType
value". I think we can just make PoseInfo<T>
generic in the particular PoseType
, and have the generic type parameter default to the full union so the type PoseInfo
without a generic type parameter is the same as before:
type PoseInfo<T extends PoseType = PoseType> = {
poseType: T,
data: number,
other: string
}
Question 1: How can we enforce that exampleArray
will contain an array with all PoseTypes
?
Answer: TypeScript doesn't have an easy way to refer to an "exhaustive array" like this. There's no specific type corresponding to that, or at least not one that scales well. There are a bunch of existing questions in SO about exhaustive arrays in TypeScript, with different approaches... such as this question or this question.
The approach I'll use here is to represent the exhaustive array as a generic constraint enforced by a helper function:
const exhaustivePoseInfoArray = <T extends PoseType>(
arr: readonly PoseInfo<PoseType extends T ? T : Exclude<PoseType, T>>[]
) => arr;
This examines the passed-in array and figures out union T
of all of the poseType
properties of its elements. If it is the full PoseType
union, everything is fine. Otherwise it will figure out which ones are missing (Exclude<PoseType, T>
) and demands that the array contain that element.
What we will do is pass a candidate array to exhaustivePoseInfoArray()
, which will just return the input. If it does so with no compiler warning, then the array has all the required elements. Otherwise, there will be some sort of warning (whether or not the warning makes sense is debatable):
const exampleArray = exhaustivePoseInfoArray([
{
poseType: 'front',
data: 1,
other: 'a'
},
{
poseType: 'side',
data: 2,
other: 'b'
},
{
poseType: 'back',
data: 3,
other: 'c'
},
]); // no error!
const badArray = exhaustivePoseInfoArray([
{ poseType: "front", data: 1, other: "a" }]) // error!
// ~~~~~~~~ Type '"front"' is not assignable to type '"side" | "back"'. ♂️
So you can see that exampleArray
is accepted, while badArray
is rejected with a warning saying that it's missing "side"
and "back"
.
Question 2: How can we enforce that requiredObject
will contain all PoseType
s with the key matching the poseType
property?
Here we can luckily at least define a specific mapped type that satisfies this requirement:
type RequiredObject = { [K in PoseType]: PoseInfo<K> }
It just says "for every K
in the PoseType
union, a RequiredObject
has a property with key K
and value PoseInfo<K>
. Let's test it out:
const requiredObject: RequiredObject = {
front: {
poseType: 'front',
data: 1,
other: 'a'
},
side: {
poseType: 'side',
data: 2,
other: 'b'
},
back: {
poseType: 'back',
data: 3,
other: 'c'
},
} // no compiler warning
const badObject: RequiredObject = {
front: {
poseType: 'front',
data: 1,
other: 'a'
},
side: {
poseType: 'front', // error!
//~~~~~~ <-- Type '"front"' is not assignable to type '"side"'
data: 2,
other: 'b'
},
back: {
poseType: 'back',
data: 3,
other: 'c'
},
}
So good, so far.
Question 3: How can we use Array.reduce()
, with proper types to achieve this type safety?
I'm afraid it's not really possible for the compiler to understand inside the implementation of a function that the exhaustive array in the answer to Question 1 can be converted into the required object in the answer to Question 2 via Array.reduce()
. This would require the compiler to understand more about the type manipulation of unspecified generic type parameters, which it can't do very well. Instead it will complain that the array might not be a PoseInfo[]
, or that the accumulator might not be a RequiredObject
, or that the value read from the array might not be appropriate to add to the accumulator at a key corresponding to its poseType
property.
Therefore we are going to use type assertions to tell the compiler that we are sure that the types work out. This shifts the burden of verifying type safety away from the compiler (which can't do it) to us (who need to be careful):
const exhaustivePoseInfoArrayToRequiredObject = <T extends PoseType>(
arr: readonly PoseInfo<PoseType extends T ? T : Exclude<PoseType, T>>[]
) => (arr as any as PoseInfo[]).reduce(
<T extends PoseType>(acc: RequiredObject, i: RequiredObject[T]) =>
((acc[i.poseType] as RequiredObject[T]) = i, acc),
{} as RequiredObject
);
This function claims to turn an exhaustive array into a RequiredObject
. Let's see if it does:
const req = exhaustivePoseInfoArrayToRequiredObject(exampleArray);
// const req: RequiredObject
console.log(req);
/* {
"front": {
"poseType": "front",
"data": 1,
"other": "a"
},
"side": {
"poseType": "side",
"data": 2,
"other": "b"
},
"back": {
"poseType": "back",
"data": 3,
"other": "c"
}
} */
Looks good!
Playground link to code
Or this link if you need no repeats as well as none missing