Can Typescript Interfaces express co-occurrence constraints for properties

Viewed 273

Is there a standard pattern within a monolithic Typescript Interface or Type definitions to assert properties either appear together or don't appear at all?

For example an item could be valid if it looked like this...

{
  id:"ljklkj",
  spellcheck:true,
  spellcheckModel:"byzantine",
}

...or this...

{
  id:"ljklkj",
}

However it would be invalid if either of the spellcheck properties occurred in isolation.

{
  id:"ljklkj",
  spellcheckModel:"byzantine",
}
{
  id:"ljklkj",
  spellcheck:true,
}

Monolithic

Of course the simple case above could be resolved by creating a Data and a SpellcheckData Type or Interface. In my application case, however, there will be more than one 'cluster' of co-occurring properties. Defining a new type for every combination of co-occurrence would lead to an explosion of types in order to express the case.

For this reason I've referred to the solution as a 'monolithic' interface. Of course it may be necessary to use some form of composition to define it.

What I've Tried

I have tried to find examples like this within the Typescript language reference, but without knowing what the feature might be called, (or indeed if it's a feature that can be expressed at all), I'm struggling. Properties can be individually optional but I can't see a way of expressing co-occurrence.

Related Technologies

An equivalent feature for XML data validation is discussed here... https://www.w3.org/wiki/Co-occurrence_constraints

For JSON I understand schema languages like Schematron and Json Content Rules are able to express co-constraints.

Worked example

If I were to imagine typescript syntax for the co-constraint case applied to HTTP parameter sets for the Solr search engine, it might look like this, indicating that you could opt in to fully satisfying the Spell or Group params, or not at all - a union in which each type is optional (indicated by the ?) ...

type SolrPassthru =
  SolrCoreParams & (
    SolrSpellParams? |
    SolrGroupParams?  
  )

This contrasts with the example below, which is I believe is correct Typescript, but requires ALL parameters from each group of parameters.

type SolrCoreParams = {
  defType: SolrDefType,
  boost: SolrBoostType,
}

type SolrSpellParams = {
  spellcheck: "true" | "false",
  "spellcheck.collate": "true" | "false",
  "spellcheck.maxCollationTries": 1,
}

type SolrGroupParams = {
  group: "true" | "false",
  "group.limit": '4'
  "group.sort": 'group_level asc,score desc,published desc,text_sort asc'
  "group.main": 'true'
  "group.field": 'group_uri'
}

type SolrPassthru =
  SolrCoreParams & 
  SolrSpellParams &
  SolrGroupParams
1 Answers

Please try the following. It seems it shows errors in the correct places.

type None<T> = {[K in keyof T]?: never}
type EitherOrBoth<T1, T2> = T1 & None<T2> | T2 & None<T1> | T1 & T2

interface Data {
  id: string;
}

interface SpellCheckData {
  spellcheck: boolean,
  spellcheckModel: string,
}

// Two interfaces
var z1: EitherOrBoth<Data, SpellCheckData> = { id: "" };
var z2: EitherOrBoth<Data, SpellCheckData> = { spellcheck: true,  spellcheckModel: 'm'};
var z3ERROR: EitherOrBoth<Data, SpellCheckData> = { spellcheck: true};
var z4: EitherOrBoth<Data, SpellCheckData> = { id: "", spellcheck: true,  spellcheckModel: 'm'};

interface MoreData {
  p1: string,
  p2: string,
  p3: string,
}

type Monolith = EitherOrBoth<Data, EitherOrBoth<SpellCheckData, MoreData>>

var x1: Monolith  = { id: "" };
var x2: Monolith  = { spellcheck: true,  spellcheckModel: 'm'};
var x3ERROR: Monolith  = { spellcheck: true};                       
var x4: Monolith  = { id: "", spellcheck: true,  spellcheckModel: 'm'};
var x5ERROR: Monolith  = { p1: ""};                                  
var x6ERROR: Monolith  = { p1: "", p2: ""};
var x7: Monolith  = { p1: "", p2: "", p3: ""};
var x8: Monolith  = { id: "", p1: "", p2: "", p3: ""};
var x9ERROR: Monolith  = { id: "", spellcheck: true, p1: "", p2: "", p3: ""};
var x10: Monolith  = { id: "", spellcheck: true, spellcheckModel: 'm', p1: "", p2: "", p3: ""};

Playground link

Update

If you prefer to pass types as a tuple, you can use the following utility:

type CombinationOf<T> = T extends [infer U1, infer U2] ? EitherOrBoth<U1, U2> :
                        T extends [infer U1, infer U2, infer U3] ? EitherOrBoth<U1, EitherOrBoth<U2, U3>> :
                        T extends [infer U1, infer U2, infer U3, infer U4] ? EitherOrBoth<U1, EitherOrBoth<U2, EitherOrBoth<U3, U4>>> :
                        never;

type Monolith = CombinationOf<[Data, SpellCheckData, MoreData]>

If some properties are required:

type Monolith = Data & CombinationOf<[Data, SpellCheckData, MoreData]>
Related