How to enforce all members of a union will be present in an array of objects? How to reduce the array to an object with all union members as keys?

    type PoseType = 'front' | 'side' | 'back';
    type PoseInfo = {
        poseType: PoseType,
        data: number,
        other: string
    }
 
    const exampleArray = [
      {
        poseType: 'front',
        data: 1,
        other: 'a'
      },
      {
        poseType: 'side',
        data: 2,
        other: 'b'
      },
      {
        poseType: 'back',
        data: 3,
        other: 'c'
      },
    ]
      
      
    const requiredObject: {[k in PoseType]: PoseInfo}= {
      front:{// <--
        poseType: 'front', //matches the above property
        data: 1,
        other: 'a'
      },
      side:{
        poseType: 'side',
        data: 2,
        other: 'b'
      },
      back:{
        poseType: 'back',
        data: 3,
        other: 'c'
      },
    }
      

How to enforce that:

  1. exampleArray will contain an array with all PoseTypes in poseType
    property
  2. requiredObject will contain all PoseTypes with object.poseType matching the property
  3. How to use Array.reduce, with proper types to achieve this type safety?

8 thoughts on “How to enforce all members of a union will be present in an array of objects? How to reduce the array to an object with all union members as keys?”

  1. 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 PoseTypes 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

    Reply
  2. You have a type PoseInfo, so you can declare your exampleArray as an array of PoseInfo objects.

    type PoseType = 'front' | 'side' | 'back';
    
    type PoseInfo = {
            poseType: PoseType,
            data: number,
            other: string
        }
    
    const exampleArray: PoseInfo[];
    
    Reply

Leave a Comment