Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can Typescript Interfaces express co-occurrence constraints for properties

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
like image 537
cefn Avatar asked Aug 17 '20 08:08

cefn


People also ask

Can we have interface with optional and default properties in TypeScript?

If you want to set the properties of an interface to have a default value of undefined , you can simply make the properties optional. Copied!

What is the use of interface in TypeScript?

Interfaces in TypeScript have two usage scenarios: you can create a contract that classes must follow, such as the members that those classes must implement, and you can also represent types in your application, just like the normal type declaration.

Can TypeScript interface have methods?

A TypeScript Interface can include method declarations using arrow functions or normal functions, it can also include properties and return types. The methods can have parameters or remain parameterless.

How do I create an instance of an interface in TypeScript?

To create an object based on an interface, declare the object's type to be the interface, e.g. const obj1: Employee = {} . The object has to conform to the property names and the type of the values in the interface, otherwise the type checker throws an error.

What is interface in typescript?

Interface can define both the kind of key an array uses and the type of entry it contains. Index can be of type string or type number. An interface can be extended by other interfaces. In other words, an interface can inherit from other interface. Typescript allows an interface to inherit from multiple interfaces.

How to use type parameters in generic constraints in typescript?

Using type parameters in generic constraints TypeScript allows you to declare a type parameter constrained by another type parameter. The following prop () function accepts an object and a property name. It returns the value of the property.

Why does typescript not work with objects only?

TypeScript doesn’t issue any error. Instead of working with all types, you may want to add a constraint to the merge () function so that it works with objects only. To do this, you need to list out the requirement as a constraint on what U and V types can be.

What is a type constraint?

A type constraint is a "rule" that narrows down the possibilities of what a generic type could be. For example, in the the send definition above, we declared a type variable T that is not constrained at all. Which is why we were able to call send with values that aren't JSON serializeable. // This compiles ...


Video Answer


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]>
like image 107
Lesiak Avatar answered Sep 23 '22 22:09

Lesiak