Search code examples
typescripttypeshl7-fhir

Can I specify a recursive property rule based on whether or not it is present in a descendant?


I want to specify a type called Item which has the possibility of being nested as follows:

Either

type Item {
    ...;
    item: Item;
    answer: {
        ...;
        //(does not contain property "item")
        }
    }

OR

type Item {
    ...;
    //(does not contain property "item")
    answer: {
        ...;
        item: Item
        }
    }

i.e. either the Item type can have item:Item as a direct child property, or as a grandchild below the property answer but not both.

Context: I'm trying to implement the item property of the QuestionnaireResponse resource from the FHIR specification


Solution

  • This should do the trick:

    type FirstOrSecondLayer<T, I, A extends string> = XOR<T & I, T & Record<A, I>>
    
    type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
    
    type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
    type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
    
    type Item = Expand<FirstOrSecondLayer<{
        a: string
        b: string
        answer: {
          c: string
        }
      },{
        item: Item
      }, "answer">
    >
    

    There is a lot of stuff going on here. I added some extra properties a, b and c for demonstration purposes. You pass the type without item as T. I is the exclusive object type. And for A you pass the name of the property which nests I.

    I used the Expand type from here and XOR from here.

    Here are some test cases:

    const t1: Item = {
      a: "",
      b: "",
      item: {} as Item,
      answer: {
        c: ""
      }
    } // works
    
    const t2: Item = {
      a: "",
      b: "",
      answer: {
        c: "",
        item: {} as Item,
      }
    } // works
    
    
    const t3: Item = {
      a: "",
      b: "",
      item: {} as Item,
      answer: {
        c: "",
        item: {} as Item, // error
      }
    } 
    

    Let me know if this works for your use case.

    Playground