Search code examples
typescriptmapped-types

Mapped types: Make property required based on whether array of same object contains string literal


Is it possible to make an object property dependent on whether an array of the same object contains a string literal?

type Operator = "A" |  "B"
type SomeStruct = {
    operators: Operator[];
    someProp: string; // this should be required if operators include "A", optional if not
}

// desired result
const structWithA: SomeStruct = {
  operators: ["A", "B"],
  someProp: "" // correct, since operators contains "A", someProp is required
};

const structWithB: SomeStruct = {
  operators: ["B"],
  // currently errors, but desired outcome is that it should not error, since operators does not contain "A"
};

declare const structs: SomeStruct[];

structs.map(struct => {
  if(struct.operators.includes("A")) {
    // someProp is safely accessible
  }

  // since .includes has a narrow type signature, maybe another way to safely access someProp is needed 
})

Solution

  • By applying a generic to SomeStruct (and using a predicate to discriminate its type), you can accomplish this:

    TS Playground

    type A = 'A';
    type B = 'B';
    type Operator = A | B;
    
    type SomeStruct<O extends Operator> = { operators: O[] } & (
      Record<'someProp', string> extends infer T ?
        A extends O ? T : Partial<T>
        : unknown
    );
    
    const s1: SomeStruct<Operator> = { operators: ['B', 'A'], someProp: '' };
    const s2: SomeStruct<Operator> = { operators: ['B', 'A'] }; // Error (2322)
    const s3: SomeStruct<A> = { operators: ['A'], someProp: '' };
    const s4: SomeStruct<A> = { operators: ['A'] }; // Error (2322)
    const s5: SomeStruct<B> = { operators: ['B'] };
    const s6: SomeStruct<B> = { operators: ['B'], someProp: '' };
    const s7: SomeStruct<A> = { operators: [] }; // Error (2322)
    const s8: SomeStruct<A> = { operators: [], someProp: '' };
    const s9: SomeStruct<B> = { operators: [], someProp: '' };
    const s10: SomeStruct<B> = { operators: [], someProp: '' };
    
    declare const structs: (SomeStruct<Operator>)[];
    
    function includesA (struct: SomeStruct<Operator>): struct is SomeStruct<A> {
      return struct.operators.includes('A');
    }
    
    structs.map(struct => {
      if(includesA(struct)) {
        struct.someProp; // string
      }
    });