Repository: FHIR/sushi (ts)

Issue:
SUSHI allows authors to put constraints on choices that have been constrained out
The following is allowed by SUSHI:
```
Profile: MyProfile
Parent: Observation
* valueCodeableConcept MS
* value[x] only Quantity
```

Ideally, SUSHI should recognize and report this as an error and exclude the `valueCodeableConcept` constraints from the profile's SD.

Pull request:
Show a message when created choices are made obsolete with type constraints
Fixes #210 with task https://standardhealthrecord.atlassian.net/browse/CIMPL-271

The sample FSH in the issue gives an example of the case being handled: when a type constraint comes after a different rule that creates a choice, and the type constraint makes that choice obsolete, that's probably a user error. So, show a message to the user indicating this. When the type constraint comes first, existing code handles that case and logs an error.

To explain a bit about the `should not log an error when a type constraint is applied to a specific slice` test case: exciting things can happen when sliced elements contain choice elements. This test shows that type constraints on one slice's choice won't cause problems for other slices.

Patch:
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index 5fe85c6..f5626c9 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -120,7 +120,7 @@ export class StructureDefinitionExporter implements Fishable {
             element.applyFlags(rule.mustSupport, rule.summary, rule.modifier);
           } else if (rule instanceof OnlyRule) {
             const target = structDef.getReferenceName(rule.path, element);
-            element.constrainType(rule.types, this, target);
+            element.constrainType(rule, this, target);
           } else if (rule instanceof ValueSetRule) {
             const vsURI = this.fishForMetadata(rule.valueSet, Type.ValueSet)?.url ?? rule.valueSet;
             element.bindToVS(vsURI, rule.strength as ElementDefinitionBindingStrength);
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 3486138..d82d0fc 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -4,7 +4,7 @@ import { minify } from 'html-minifier';
 import { StructureDefinition } from './StructureDefinition';
 import { CodeableConcept, Coding, Quantity, Ratio, Reference } from './dataTypes';
 import { FshCode, FshRatio, FshQuantity, FshReference } from '../fshtypes';
-import { FixedValueType } from '../fshtypes/rules';
+import { FixedValueType, OnlyRule } from '../fshtypes/rules';
 import {
   BindingStrengthError,
   CodedTypeNotFoundError,
@@ -459,8 +459,7 @@ export class ElementDefinition {
    * - specifying a target that does not match any of the existing type choices
    * - specifying types or a target whose definition cannot be found
    * @see {@link http://hl7.org/fhir/R4/elementdefinition-definitions.html#ElementDefinition.type}
-   * @param {{ type: string; isReference?: boolean }[]} types - the set of constrained types,
-   *   identified by id/type/url strings and an optional reference flag (defaults false)
+   * @param {OnlyRule} rule - The rule specifying the types to apply
    * @param {Fishable} fisher - A fishable implementation for finding definitions and metadata
    * @param {string} [target] - a specific target type to constrain.  If supplied, will attempt to
    *   constrain only that type without affecting other types (in a choice or reference to a choice).
@@ -468,11 +467,8 @@ export class ElementDefinition {
    * @throws {InvalidTypeError} when a passed in type or the targetType doesn't match any existing
    *   types
    */
-  constrainType(
-    types: { type: string; isReference?: boolean }[],
-    fisher: Fishable,
-    target?: string
-  ): void {
+  constrainType(rule: OnlyRule, fisher: Fishable, target?: string): void {
+    const types = rule.types;
     // Establish the target types (if applicable)
     const targetType = this.getTargetType(target, fisher);
     const targetTypes: ElementDefinitionType[] = targetType ? [targetType] : this.type;
@@ -489,6 +485,7 @@ export class ElementDefinition {
 
     // Loop through the existing element types building the new set of element types w/ constraints
     const newTypes: ElementDefinitionType[] = [];
+    const oldTypes: ElementDefinitionType[] = [];
     for (const type of this.type) {
       // If the typeMatches map doesn't have the type code at all, this means that a target was
       // specified, and this element type wasn't the target.  In this case, we want to keep it.
@@ -501,6 +498,7 @@ export class ElementDefinition {
       // element type should be filtered out of the results, so just skip to the next one.
       const matches = typeMatches.get(type.code);
       if (isEmpty(matches)) {
+        oldTypes.push(type);
         continue;
       }
 
@@ -531,6 +529,17 @@ export class ElementDefinition {
       }
     }
 
+    // Let user know if other rules have been made obsolete
+    const obsoleteChoices = this.structDef.findObsoleteChoices(this, oldTypes);
+    if (obsoleteChoices.length > 0) {
+      logger.error(
+        `Type constraint on ${this.path} makes rules in ${
+          this.structDef.name
+        } obsolete for choices: ${obsoleteChoices.join(', ')}`,
+        rule.sourceInfo
+      );
+    }
+
     // Finally, reset this element's types to the new types
     this.type = newTypes;
   }
diff --git a/src/fhirtypes/StructureDefinition.ts b/src/fhirtypes/StructureDefinition.ts
index 69d78b8..43c3569 100644
--- a/src/fhirtypes/StructureDefinition.ts
+++ b/src/fhirtypes/StructureDefinition.ts
@@ -560,7 +560,7 @@ export class StructureDefinition {
         // if we do not have a matching slice, we want to slice the first match
         const matchingXElement = matchingXElements[0];
         // If we find a matching [x] element, we need to slice it to create the child element
-        // NOTE: The spec is somewhat incosistent on handling choice slicing, we decided on this
+        // NOTE: The spec is somewhat inconsistent on handling choice slicing, we decided on this
         // approach per consistency with 4.0.1 observation-vitalsigns profiles and per this post
         // https://blog.fire.ly/2019/09/13/type-slicing-in-fhir-r4/.
         matchingXElement.sliceIt('type', '$this', false, 'open');
@@ -598,6 +598,29 @@ export class StructureDefinition {
     return matchingSlice;
   }
 
+  findObsoleteChoices(baseElement: ElementDefinition, oldTypes: ElementDefinitionType[]): string[] {
+    // first, find all the elements representing choices for the same choice element
+    const parentId = baseElement.parent().id;
+    const choiceElements = this.elements.filter(e => {
+      return e.path === baseElement.path && e.id.startsWith(parentId);
+    });
+    const matchedThings: ElementDefinition[] = [];
+    const desiredSliceName = baseElement.path
+      .slice(baseElement.path.lastIndexOf('.') + 1)
+      .slice(0, -3);
+    // an obsolete choice is one where:
+    // 1. the choice represents a type being constrained out, and
+    // 2. a rule has been applied to that choice
+    oldTypes.forEach(oldType => {
+      matchedThings.push(
+        ...choiceElements.filter(e => {
+          return e.sliceName == `${desiredSliceName}${upperFirst(oldType.code)}` && e.hasDiff();
+        })
+      );
+    });
+    return matchedThings.map(e => e.sliceName);
+  }
+
   /**
    * Looks for a Reference type element within the set of elements that matches the fhirPath
    * @param {PathPart} pathPart - The path to match the Reference type elements against


Test patch:
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index e75bea8..acba8e6 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -1019,6 +1019,98 @@ describe('StructureDefinitionExporter', () => {
     expect(loggerSpy.getLastMessage()).toMatch(/File: Only\.fsh.*Line: 10\D*/s);
   });
 
+  it('should log an error when a type constraint implicitly removes a choice created in the current StructureDefinition', () => {
+    const flagFirst = new Profile('FlagFirst');
+    flagFirst.parent = 'Observation';
+    const flagRule = new FlagRule('valueCodeableConcept')
+      .withFile('FlagFirst.fsh')
+      .withLocation([7, 12, 7, 31]);
+    flagRule.mustSupport = true;
+    const secondFlagRule = new FlagRule('valueString')
+      .withFile('FlagFirst.fsh')
+      .withLocation([8, 12, 8, 31]);
+    secondFlagRule.mustSupport = true;
+    const onlyRule = new OnlyRule('value[x]')
+      .withFile('FlagFirst.fsh')
+      .withLocation([9, 12, 9, 24]);
+    onlyRule.types = [{ type: 'Quantity' }, { type: 'string' }];
+
+    flagFirst.rules.push(flagRule); // * valueCodeableConcept MS
+    flagFirst.rules.push(secondFlagRule); // * valueString MS
+    flagFirst.rules.push(onlyRule); // value[x] only Quantity or String
+
+    exporter.exportStructDef(flagFirst);
+    const sd = pkg.profiles[0];
+    const constrainedValue = sd.findElement('Observation.value[x]');
+    expect(constrainedValue.type).toHaveLength(2);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: FlagFirst\.fsh.*Line: 9\D*/s);
+  });
+
+  it('should not log an error when a type constraint implicitly removes a choice that has no rules applied in the current StructureDefinition', () => {
+    loggerSpy.reset();
+    const parentProfile = new Profile('ParentProfile');
+    parentProfile.parent = 'Observation';
+    const flagRule = new FlagRule('valueCodeableConcept');
+    flagRule.mustSupport = true;
+    parentProfile.rules.push(flagRule); // * valueCodeableConcept MS
+
+    const childProfile = new Profile('ChildProfile');
+    childProfile.parent = 'ParentProfile';
+    const onlyRule = new OnlyRule('value[x]');
+    onlyRule.types = [{ type: 'Quantity' }];
+    childProfile.rules.push(onlyRule); // * value[x] only Quantity
+    exporter.exportStructDef(parentProfile);
+    exporter.exportStructDef(childProfile);
+    expect(pkg.profiles).toHaveLength(2);
+    expect(loggerSpy.getAllLogs()).toHaveLength(0);
+  });
+
+  it('should not log an error when a type constraint is applied to a specific slice', () => {
+    loggerSpy.reset();
+    const profile = new Profile('ConstrainedObservation');
+    profile.parent = 'Observation';
+    // * component ^slicing.discriminator[0].type = #pattern
+    // * component ^slicing.discriminator[0].path = "code"
+    // * component ^slicing.rules = #open
+    // * component contains FirstSlice and SecondSlice
+    // * component[FirstSlice].value[x] only Quantity
+    // * component[FirstSlice].valueQuantity 1..1
+    // * component[SecondSlice].value[x] only string
+    const slicingType = new CaretValueRule('component');
+    slicingType.caretPath = 'slicing.discriminator[0].type';
+    slicingType.value = new FshCode('pattern');
+    const slicingPath = new CaretValueRule('component');
+    slicingPath.caretPath = 'slicing.discriminator[0].path';
+    slicingPath.value = 'code';
+    const slicingRules = new CaretValueRule('component');
+    slicingRules.caretPath = 'slicing.rules';
+    slicingRules.value = new FshCode('open');
+    const componentSlices = new ContainsRule('component');
+    componentSlices.items = ['FirstSlice', 'SecondSlice'];
+    const firstType = new OnlyRule('component[FirstSlice].value[x]');
+    firstType.types = [{ type: 'Quantity' }];
+    const firstCard = new CardRule('component[FirstSlice].valueQuantity');
+    firstCard.min = 1;
+    firstCard.max = '1';
+    const secondType = new OnlyRule('component[SecondSlice].value[x]');
+    secondType.types = [{ type: 'string' }];
+
+    profile.rules.push(
+      slicingType,
+      slicingPath,
+      slicingRules,
+      componentSlices,
+      firstType,
+      firstCard,
+      secondType
+    );
+
+    exporter.exportStructDef(profile);
+    const sd = pkg.profiles[0];
+    expect(sd).toBeTruthy();
+    expect(loggerSpy.getAllLogs()).toHaveLength(0);
+  });
+
   // Fixed Value Rule
   it('should apply a correct FixedValueRule', () => {
     const profile = new Profile('Foo');
diff --git a/test/fhirtypes/ElementDefinition.constrainType.test.ts b/test/fhirtypes/ElementDefinition.constrainType.test.ts
index 6bff2ce..178ff9d 100644
--- a/test/fhirtypes/ElementDefinition.constrainType.test.ts
+++ b/test/fhirtypes/ElementDefinition.constrainType.test.ts
@@ -6,6 +6,7 @@ import { ElementDefinitionType } from '../../src/fhirtypes';
 import { Type } from '../../src/utils';
 import cloneDeep from 'lodash/cloneDeep';
 import path from 'path';
+import { OnlyRule } from '../../src/fshtypes/rules';
 
 describe('ElementDefinition', () => {
   let defs: FHIRDefinitions;
@@ -27,7 +28,9 @@ describe('ElementDefinition', () => {
   describe('#constrainType()', () => {
     it('should allow a choice to be constrained to a subset', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType([{ type: 'Quantity' }, { type: 'integer' }], fisher);
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'Quantity' }, { type: 'integer' }];
+      valueX.constrainType(valueConstraint, fisher);
       expect(valueX.type).toHaveLength(2);
       expect(valueX.type[0]).toEqual(new ElementDefinitionType('Quantity'));
       expect(valueX.type[1]).toEqual(new ElementDefinitionType('integer'));
@@ -35,10 +38,14 @@ describe('ElementDefinition', () => {
 
     it('should maintain original type order when constraining to a subset', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType(
-        [{ type: 'Period' }, { type: 'integer' }, { type: 'Quantity' }, { type: 'Ratio' }],
-        fisher
-      );
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [
+        { type: 'Period' },
+        { type: 'integer' },
+        { type: 'Quantity' },
+        { type: 'Ratio' }
+      ];
+      valueX.constrainType(valueConstraint, fisher);
       expect(valueX.type).toHaveLength(4);
       expect(valueX.type[0]).toEqual(new ElementDefinitionType('Quantity'));
       expect(valueX.type[1]).toEqual(new ElementDefinitionType('integer'));
@@ -48,21 +55,27 @@ describe('ElementDefinition', () => {
 
     it('should allow a choice to be constrained to a single item', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType([{ type: 'Quantity' }], fisher);
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'Quantity' }];
+      valueX.constrainType(valueConstraint, fisher);
       expect(valueX.type).toHaveLength(1);
       expect(valueX.type[0]).toEqual(new ElementDefinitionType('Quantity'));
     });
 
     it('should allow a choice to be constrained to a single item by its URL', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType([{ type: 'http://hl7.org/fhir/StructureDefinition/Quantity' }], fisher);
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'http://hl7.org/fhir/StructureDefinition/Quantity' }];
+      valueX.constrainType(valueConstraint, fisher);
       expect(valueX.type).toHaveLength(1);
       expect(valueX.type[0]).toEqual(new ElementDefinitionType('Quantity'));
     });
 
     it('should allow a choice to be constrained to a single profile', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType([{ type: 'SimpleQuantity' }], fisher);
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'SimpleQuantity' }];
+      valueX.constrainType(valueConstraint, fisher);
       expect(valueX.type).toHaveLength(1);
       expect(valueX.type[0]).toEqual(
         new ElementDefinitionType('Quantity').withProfiles(
@@ -73,7 +86,9 @@ describe('ElementDefinition', () => {
 
     it('should allow a resource type to be constrained to multiple profiles', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType([{ type: 'SimpleQuantity' }, { type: 'MoneyQuantity' }], fisher);
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'SimpleQuantity' }, { type: 'MoneyQuantity' }];
+      valueX.constrainType(valueConstraint, fisher);
       expect(valueX.type).toHaveLength(1);
       expect(valueX.type[0]).toEqual(
         new ElementDefinitionType('Quantity').withProfiles(
@@ -90,7 +105,9 @@ describe('ElementDefinition', () => {
     it('should allow Resource to be constrained to a resource', () => {
       const bundle = fisher.fishForStructureDefinition('Bundle');
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
-      entryResource.constrainType([{ type: 'Patient' }], fisher);
+      const resourceConstraint = new OnlyRule('entry.resource');
+      resourceConstraint.types = [{ type: 'Patient' }];
+      entryResource.constrainType(resourceConstraint, fisher);
       expect(entryResource.type).toHaveLength(1);
       expect(entryResource.type[0]).toEqual(new ElementDefinitionType('Patient'));
     });
@@ -98,7 +115,9 @@ describe('ElementDefinition', () => {
     it('should allow Resource to be constrained to a profile', () => {
       const bundle = fisher.fishForStructureDefinition('Bundle');
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
-      entryResource.constrainType([{ type: 'http://hl7.org/fhir/StructureDefinition/bp' }], fisher);
+      const resourceConstraint = new OnlyRule('entry.resource');
+      resourceConstraint.types = [{ type: 'http://hl7.org/fhir/StructureDefinition/bp' }];
+      entryResource.constrainType(resourceConstraint, fisher);
       expect(entryResource.type).toHaveLength(1);
       expect(entryResource.type[0]).toEqual(
         new ElementDefinitionType('Observation').withProfiles(
@@ -110,16 +129,15 @@ describe('ElementDefinition', () => {
     it('should allow Resource to be constrained to multiple resources and profiles', () => {
       const bundle = fisher.fishForStructureDefinition('Bundle');
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
-      entryResource.constrainType(
-        [
-          { type: 'Practitioner' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/Procedure' }
-        ],
-        fisher
-      );
+      const resourceConstraint = new OnlyRule('entry.resource');
+      resourceConstraint.types = [
+        { type: 'Practitioner' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/Procedure' }
+      ];
+      entryResource.constrainType(resourceConstraint, fisher);
       expect(entryResource.type).toHaveLength(4);
       expect(entryResource.type[0]).toEqual(new ElementDefinitionType('Practitioner'));
       expect(entryResource.type[1]).toEqual(
@@ -138,7 +156,9 @@ describe('ElementDefinition', () => {
 
     it('should allow a choice to be constrained such that only the target type is constrained to a profile and others remain as-is', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
-      valueX.constrainType([{ type: 'SimpleQuantity' }], fisher, 'Quantity');
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'SimpleQuantity' }];
+      valueX.constrainType(valueConstraint, fisher, 'Quantity');
       expect(valueX.type).toHaveLength(11);
       expect(valueX.type[0]).toEqual(
         new ElementDefinitionType('Quantity').withProfiles(
@@ -154,17 +174,15 @@ describe('ElementDefinition', () => {
     it('should allow Resource to be constrained to multiple resources and profiles when it is specifically targeted', () => {
       const bundle = fisher.fishForStructureDefinition('Bundle');
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
-      entryResource.constrainType(
-        [
-          { type: 'Practitioner' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic' },
-          { type: 'http://hl7.org/fhir/StructureDefinition/Procedure' }
-        ],
-        fisher,
-        'Resource'
-      );
+      const resourceConstraint = new OnlyRule('entry.resource');
+      resourceConstraint.types = [
+        { type: 'Practitioner' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic' },
+        { type: 'http://hl7.org/fhir/StructureDefinition/Procedure' }
+      ];
+      entryResource.constrainType(resourceConstraint, fisher, 'Resource');
       expect(entryResource.type).toHaveLength(4);
       expect(entryResource.type[0]).toEqual(new ElementDefinitionType('Practitioner'));
       expect(entryResource.type[1]).toEqual(
@@ -183,13 +201,12 @@ describe('ElementDefinition', () => {
 
     it('should allow a reference to multiple resource types to be constrained to a reference to a subset', () => {
       const performer = observation.elements.find(e => e.id === 'Observation.performer');
-      performer.constrainType(
-        [
-          { type: 'Practitioner', isReference: true },
-          { type: 'Organization', isReference: true }
-        ],
-        fisher
-      );
+      const performerConstraint = new OnlyRule('performer');
+      performerConstraint.types = [
+        { type: 'Practitioner', isReference: true },
+        { type: 'Organization', isReference: true }
+      ];
+      performer.constrainType(performerConstraint, fisher);
       expect(performer.type).toHaveLength(1);
       expect(performer.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -201,7 +218,9 @@ describe('ElementDefinition', () => {
 
     it('should allow a reference to multiple resource types to be constrained to a reference to a single type', () => {
       const performer = observation.elements.find(e => e.id === 'Observation.performer');
-      performer.constrainType([{ type: 'Organization', isReference: true }], fisher);
+      const performerConstraint = new OnlyRule('performer');
+      performerConstraint.types = [{ type: 'Organization', isReference: true }];
+      performer.constrainType(performerConstraint, fisher);
       expect(performer.type).toHaveLength(1);
       expect(performer.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -212,10 +231,11 @@ describe('ElementDefinition', () => {
 
     it('should allow a resource type in a reference to multiple types to be constrained to a single profile', () => {
       const subject = observation.elements.find(e => e.id === 'Observation.subject');
-      subject.constrainType(
-        [{ type: 'http://hl7.org/fhir/StructureDefinition/actualgroup', isReference: true }],
-        fisher
-      );
+      const subjectConstraint = new OnlyRule('subject');
+      subjectConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/actualgroup', isReference: true }
+      ];
+      subject.constrainType(subjectConstraint, fisher);
       expect(subject.type).toHaveLength(1);
       expect(subject.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -226,13 +246,12 @@ describe('ElementDefinition', () => {
 
     it('should allow a resource type in a reference to multiple types to be constrained to multiple profiles', () => {
       const hasMember = observation.elements.find(e => e.id === 'Observation.hasMember');
-      hasMember.constrainType(
-        [
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight', isReference: true }
-        ],
-        fisher
-      );
+      const hasMemberConstraint = new OnlyRule('hasMember');
+      hasMemberConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight', isReference: true }
+      ];
+      hasMember.constrainType(hasMemberConstraint, fisher);
       expect(hasMember.type).toHaveLength(1);
       expect(hasMember.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -244,13 +263,12 @@ describe('ElementDefinition', () => {
 
     it('should allow a resource type in a reference to multiple types to be constrained to a resource and a single profile', () => {
       const subject = observation.elements.find(e => e.id === 'Observation.subject');
-      subject.constrainType(
-        [
-          { type: 'Patient', isReference: true },
-          { type: 'http://hl7.org/fhir/StructureDefinition/actualgroup', isReference: true }
-        ],
-        fisher
-      );
+      const subjectConstraint = new OnlyRule('subject');
+      subjectConstraint.types = [
+        { type: 'Patient', isReference: true },
+        { type: 'http://hl7.org/fhir/StructureDefinition/actualgroup', isReference: true }
+      ];
+      subject.constrainType(subjectConstraint, fisher);
       expect(subject.type).toHaveLength(1);
       expect(subject.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -267,13 +285,12 @@ describe('ElementDefinition', () => {
       );
       const vitalSigns = StructureDefinition.fromJSON(jsonVitalSigns);
       const hasMember = vitalSigns.elements.find(e => e.id === 'Observation.hasMember');
-      hasMember.constrainType(
-        [
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight', isReference: true }
-        ],
-        fisher
-      );
+      const hasMemberConstraint = new OnlyRule('hasMember');
+      hasMemberConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight', isReference: true }
+      ];
+      hasMember.constrainType(hasMemberConstraint, fisher);
       expect(hasMember.type).toHaveLength(1);
       expect(hasMember.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -285,7 +302,9 @@ describe('ElementDefinition', () => {
 
     it('should allow a reference to Any to be constrained to a reference to a resource', () => {
       const focus = observation.elements.find(e => e.id === 'Observation.focus');
-      focus.constrainType([{ type: 'Practitioner', isReference: true }], fisher);
+      const focusConstraint = new OnlyRule('focus');
+      focusConstraint.types = [{ type: 'Practitioner', isReference: true }];
+      focus.constrainType(focusConstraint, fisher);
       expect(focus.type).toHaveLength(1);
       expect(focus.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -296,10 +315,11 @@ describe('ElementDefinition', () => {
 
     it('should allow a reference to Any to be constrained to a reference to a profile', () => {
       const focus = observation.elements.find(e => e.id === 'Observation.focus');
-      focus.constrainType(
-        [{ type: 'http://hl7.org/fhir/StructureDefinition/bp', isReference: true }],
-        fisher
-      );
+      const focusConstraint = new OnlyRule('focus');
+      focusConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/bp', isReference: true }
+      ];
+      focus.constrainType(focusConstraint, fisher);
       expect(focus.type).toHaveLength(1);
       expect(focus.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -310,13 +330,12 @@ describe('ElementDefinition', () => {
 
     it('should allow a reference to Any to be constrained to multiple references', () => {
       const focus = observation.elements.find(e => e.id === 'Observation.focus');
-      focus.constrainType(
-        [
-          { type: 'Practitioner', isReference: true },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bp', isReference: true }
-        ],
-        fisher
-      );
+      const focusConstraint = new OnlyRule('focus');
+      focusConstraint.types = [
+        { type: 'Practitioner', isReference: true },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bp', isReference: true }
+      ];
+      focus.constrainType(focusConstraint, fisher);
       expect(focus.type).toHaveLength(1);
       expect(focus.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -328,14 +347,12 @@ describe('ElementDefinition', () => {
 
     it('should allow a reference to multiple resource types to be constrained such that only the target reference is constrained and others remain as-is', () => {
       const hasMember = observation.elements.find(e => e.id === 'Observation.hasMember');
-      hasMember.constrainType(
-        [
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true },
-          { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight', isReference: true }
-        ],
-        fisher,
-        'Observation'
-      );
+      const hasMemberConstraint = new OnlyRule('hasMember');
+      hasMemberConstraint.types = [
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true },
+        { type: 'http://hl7.org/fhir/StructureDefinition/bodyweight', isReference: true }
+      ];
+      hasMember.constrainType(hasMemberConstraint, fisher, 'Observation');
       expect(hasMember.type).toHaveLength(1);
       expect(hasMember.type[0]).toEqual(
         new ElementDefinitionType('Reference').withTargetProfiles(
@@ -351,7 +368,9 @@ describe('ElementDefinition', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
       const clone = cloneDeep(valueX);
       expect(() => {
-        clone.constrainType([{ type: 'decimal' }], fisher);
+        const valueConstraint = new OnlyRule('value[x]');
+        valueConstraint.types = [{ type: 'decimal' }];
+        clone.constrainType(valueConstraint, fisher);
       }).toThrow(/"decimal" does not match .* Quantity or CodeableConcept or string/);
       expect(clone).toEqual(valueX);
     });
@@ -360,7 +379,9 @@ describe('ElementDefinition', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.performer');
       const clone = cloneDeep(valueX);
       expect(() => {
-        clone.constrainType([{ type: 'Medication', isReference: true }], fisher);
+        const performerConstraint = new OnlyRule('performer');
+        performerConstraint.types = [{ type: 'Medication', isReference: true }];
+        clone.constrainType(performerConstraint, fisher);
       }).toThrow(
         /"Reference\(Medication\)" does not match .* Reference\(http:\/\/hl7.org\/fhir\/StructureDefinition\/Practitioner | http:\/\/hl7.org\/fhir\/StructureDefinition\/PractitionerRole .*\)/
       );
@@ -372,7 +393,9 @@ describe('ElementDefinition', () => {
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
       const clone = cloneDeep(entryResource);
       expect(() => {
-        clone.constrainType([{ type: 'Procedure', isReference: true }], fisher);
+        const resourceConstraint = new OnlyRule('entry.resource');
+        resourceConstraint.types = [{ type: 'Procedure', isReference: true }];
+        clone.constrainType(resourceConstraint, fisher);
       }).toThrow(/"Reference\(Procedure\)" does not match .* Resource/);
       expect(clone).toEqual(entryResource);
     });
@@ -381,16 +404,14 @@ describe('ElementDefinition', () => {
       const hasMember = observation.elements.find(e => e.id === 'Observation.hasMember');
       const clone = cloneDeep(hasMember);
       expect(() => {
-        clone.constrainType(
-          [
-            {
-              type: 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic',
-              isReference: true
-            }
-          ],
-          fisher,
-          'FamilyMemberHistory'
-        );
+        const hasMemberConstraint = new OnlyRule('hasMember');
+        hasMemberConstraint.types = [
+          {
+            type: 'http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic',
+            isReference: true
+          }
+        ];
+        clone.constrainType(hasMemberConstraint, fisher, 'FamilyMemberHistory');
       }).toThrow(
         /"FamilyMemberHistory" does not match .* Reference\(http:\/\/hl7.org\/fhir\/StructureDefinition\/Observation .*\)/
       );
@@ -401,7 +422,9 @@ describe('ElementDefinition', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
       const clone = cloneDeep(valueX);
       expect(() => {
-        clone.constrainType([{ type: 'SimpleQuantity' }], fisher, 'CodeableConcept');
+        const valueConstraint = new OnlyRule('value[x]');
+        valueConstraint.types = [{ type: 'SimpleQuantity' }];
+        clone.constrainType(valueConstraint, fisher, 'CodeableConcept');
       }).toThrow(/"SimpleQuantity" does not match .* CodeableConcept/);
       expect(clone).toEqual(valueX);
     });
@@ -410,11 +433,11 @@ describe('ElementDefinition', () => {
       const hasMember = observation.elements.find(e => e.id === 'Observation.hasMember');
       const clone = cloneDeep(hasMember);
       expect(() => {
-        clone.constrainType(
-          [{ type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true }],
-          fisher,
-          'QuestionnaireResponse'
-        );
+        const hasMemberConstraint = new OnlyRule('hasMember');
+        hasMemberConstraint.types = [
+          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true }
+        ];
+        clone.constrainType(hasMemberConstraint, fisher, 'QuestionnaireResponse');
       }).toThrow(
         /"Reference\(http:\/\/hl7.org\/fhir\/StructureDefinition\/bodyheight\)" does not match .* Reference\(http:\/\/hl7.org\/fhir\/StructureDefinition\/QuestionnaireResponse\)/
       );
@@ -426,7 +449,9 @@ describe('ElementDefinition', () => {
       const entryResource = bundle.elements.find(e => e.id === 'Bundle.entry.resource');
       const clone = cloneDeep(entryResource);
       expect(() => {
-        clone.constrainType([{ type: 'Procedure', isReference: true }], fisher, 'Resource');
+        const resourceConstraint = new OnlyRule('entry.resource');
+        resourceConstraint.types = [{ type: 'Procedure', isReference: true }];
+        clone.constrainType(resourceConstraint, fisher, 'Resource');
       }).toThrow(/"Reference\(Procedure\)" does not match .* Resource/);
       expect(clone).toEqual(entryResource);
     });
@@ -435,7 +460,9 @@ describe('ElementDefinition', () => {
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
       const clone = cloneDeep(valueX);
       expect(() => {
-        clone.constrainType([{ type: 'Quantity' }, { type: 'Monocle' }], fisher);
+        const valueConstraint = new OnlyRule('value[x]');
+        valueConstraint.types = [{ type: 'Quantity' }, { type: 'Monocle' }];
+        clone.constrainType(valueConstraint, fisher);
       }).toThrow(/No definition for the type "Monocle" could be found./);
       expect(clone).toEqual(valueX);
     });
@@ -444,13 +471,12 @@ describe('ElementDefinition', () => {
       const performer = observation.elements.find(e => e.id === 'Observation.performer');
       const clone = cloneDeep(performer);
       expect(() => {
-        clone.constrainType(
-          [
-            { type: 'Practitioner', isReference: true },
-            { type: 'Juggler', isReference: true }
-          ],
-          fisher
-        );
+        const performerConstraint = new OnlyRule('performer');
+        performerConstraint.types = [
+          { type: 'Practitioner', isReference: true },
+          { type: 'Juggler', isReference: true }
+        ];
+        clone.constrainType(performerConstraint, fisher);
       }).toThrow(/No definition for the type "Juggler" could be found./);
       expect(clone).toEqual(performer);
     });
@@ -459,11 +485,11 @@ describe('ElementDefinition', () => {
       const hasMember = observation.elements.find(e => e.id === 'Observation.hasMember');
       const clone = cloneDeep(hasMember);
       expect(() => {
-        clone.constrainType(
-          [{ type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true }],
-          fisher,
-          'VitalBillboards'
-        );
+        const hasMemberConstraint = new OnlyRule('hasMember');
+        hasMemberConstraint.types = [
+          { type: 'http://hl7.org/fhir/StructureDefinition/bodyheight', isReference: true }
+        ];
+        clone.constrainType(hasMemberConstraint, fisher, 'VitalBillboards');
       }).toThrow(/No definition for the type "VitalBillboards" could be found./);
       expect(clone).toEqual(hasMember);
     });
diff --git a/test/fhirtypes/StructureDefinition.test.ts b/test/fhirtypes/StructureDefinition.test.ts
index 1be8b18..af7f5bc 100644
--- a/test/fhirtypes/StructureDefinition.test.ts
+++ b/test/fhirtypes/StructureDefinition.test.ts
@@ -7,6 +7,7 @@ import { ElementDefinition, ElementDefinitionType } from '../../src/fhirtypes/El
 import { TestFisher } from '../testhelpers';
 import { FshCode } from '../../src/fshtypes';
 import { Type } from '../../src/utils/Fishable';
+import { OnlyRule } from '../../src/fshtypes/rules';
 
 describe('StructureDefinition', () => {
   let defs: FHIRDefinitions;
@@ -158,7 +159,9 @@ describe('StructureDefinition', () => {
       // constrain value[x] to only a Quantity or string and give each its own short
       const valueX = observation.elements.find(e => e.id === 'Observation.value[x]');
       valueX.sliceIt('type', '$this', false, 'open');
-      valueX.constrainType([{ type: 'Quantity' }, { type: 'string' }], fisher);
+      const valueConstraint = new OnlyRule('value[x]');
+      valueConstraint.types = [{ type: 'Quantity' }, { type: 'string' }];
+      valueX.constrainType(valueConstraint, fisher);
       const valueQuantity = valueX.addSlice('valueQuantity', new ElementDefinitionType('Quantity'));
       valueQuantity.short = 'the quantity choice';
       const valueString = valueX.addSlice('valueString', new ElementDefinitionType('string'));