How to specify in TypeScript that if one key is present, another key must also be present, without redundantly reproducing the entire structure

In my code, I have a custom type defined like this (but it's not working):

type MyType =
  | {
      foo: string;
    }
  | {
      foo: string;
      barPre: string;
      barPost: string;
    }
  | {
      foo: string;
      quxPre: string;
      quxPost: string;
    }
  | {
      foo: string;
      barPre: string;
      barPost: string;
      quxPre: string;
      quxPost: string;
    };

I need objects to fit the structure of MyType as follows:

const myThing1: MyType = { foo: 'foo' };
const myThing2: MyType = { foo: 'foo', barPre: 'barPre', barPost: 'barPost' };
const myThing3: MyType = { foo: 'foo', quxPre: 'quxPre', quxPost: 'quxPost' };
const myThing4: MyType = {
  foo: 'foo',
  barPre: 'barPre',
  barPost: 'barPost',
  quxPre: 'quxPre',
  quxPost: 'quxPost',
};

Objects that do not conform to these rules are considered invalid MyTypes:

const myThing5: MyType = {}; // missing 'foo'
const myThing6: MyType = { barPre: 'barPre', barPost: 'barPost' }; // missing 'foo'
const myThing7: MyType = { foo: 'foo', barPre: 'barPre' }; // if `barPre` exists, so must `barPost`
const myThing8: MyType = { barPre: 'barPre', barPost: 'barPost', quxPre: 'quxPre', quxPost: 'quxPost' } 

I managed to get the correct type by defining it in a more complex way:

type MyType = {
  foo: string;
} & ({
  barPre?: undefined;
  barPost?: undefined;
} | {
  barPre: string;
  barPost: string;
}) & ({
  quxPre?: undefined;
  quxPost?: undefined;
} | {
  quxPre: string;
  quxPost: string;
});

However, this approach seems cumbersome. Is there a simpler method to achieve the same result?

Answer №1

This strange behavior is a result of the rigorous excess property checking that Typescript performs on unions. For more details, you can refer to this link.

Fortunately, there is a solution in the form of a strict union type definition that may work for your specific case. While I haven't personally tested it extensively, you can learn more about it by visiting this resource.

To implement this, define a StrictUnion type as follows:

type UnionKeys<T> = T extends T ? keyof T : never
type StrictUnionHelper<T, TAll> = T extends T ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>> : never
type StrictUnion<T> = StrictUnionHelper<T, T>

Then utilize this type in your union:

type MyType = StrictUnion<A | B | C | D>

By doing so, you should see errors being reported for myThing5, 6, 7 and 8 as expected.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for testing two conditions in Angular ngIf

I am facing a little issue trying to make this *ngIf statement work as expected. My goal is to display the div only if it is empty and the user viewing it is the owner. If the user is a guest and the div is empty, then it should not be shown. Here is my cu ...

How can we direct the user to another tab in Angular Mat Tab using a child component?

Within my Angular page, I have implemented 4 tabs using mat-tab. Each tab contains a child component that encapsulates smaller components to cater to the specific functionality of that tab. Now, I am faced with the challenge of navigating the user from a ...

In fact, retrieve the file from an S3 bucket and save it to your local

I've been attempting to retrieve an s3 file from my bucket using this function: async Export() { const myKey = '...key...' const mySecret = '...secret...' AWS.config.update( { accessKeyId: myKey, secretAcces ...

The "isActive" value does not share any properties with the type 'Properties<string | number, string & {}>'. This issue was encountered while using React with TypeScript

I'm attempting to include the isActive parameter inside NavLink of react-router-dom version 5, but I'm encountering two errors. The type '({ isActive }: { isActive: any; }) => { color: string; background: string; }' does not have an ...

Warning from Cytoscape.js: "The use of `label` for setting the width of a node is no longer supported. Please update your style settings for the node width." This message appears when attempting to create

I'm currently utilizing Cytoscape.js for rendering a dagre layout graph. When it comes to styling the node, I am using the property width: label in the code snippet below: const cy = cytoscape({ container: document.getElementById('cyGraph&apo ...

What causes the variable to be undefined in the method but not in the constructor in Typescript?

I am currently working on an application using AngularJS 1.4.9 with Typescript. In one of my controllers, I have injected the testManagementService service. The issue I'm facing is that while the testManagementService variable is defined as an object ...

Generate a new entry by analyzing components from a separate array in a single line

I have a list of essential items and I aim to generate a record based on the elements within that list. Each item in the required list will correspond to an empty array in the exist record. Essentially, I am looking to condense the following code into one ...

Exploring the Factory Design Pattern Together with Dependency Injection in Angular

I'm currently implementing the factory design pattern in Angular, but I feel like I might be missing something or perhaps there's a more efficient approach. My current setup involves a factory that returns a specific car class based on user input ...

Error: Type '() => () => Promise<void>' is not compatible with type 'EffectCallback'

Here is the code that I'm currently working with: useEffect(() => { document.querySelector('body').style['background-color'] = 'rgba(173, 232, 244,0.2)'; window.$crisp.push(['do', 'ch ...

typescript loop with a callback function executed at the conclusion

I am struggling with this code and it's driving me crazy. addUpSpecificDaysOfWeek(daysInMonth: any, callbackFunction: any){ var data = []; var that = this; daysMonth.forEach(function(day){ that.statsService.fetchData(that.userid, d ...

Encountering Error when Attempting to Load Webpack Dependencies for Browser

I'm currently in the process of manually scaffolding an Angular 6 app (not using the CLI). Everything was going smoothly until I encountered a webpack error: ERROR in window is not defined After researching online, it seems like I may be missing som ...

Exploring Transformation in Angular

I am looking to enhance my understanding of how ChangeDetection works, and I have a query in this regard. When using changeDetection: ChangeDetectionStrategy.OnPush, do I also need to check if currentValue exists in the ngOnChanges lifecycle hook, or is i ...

Exploring date comparisons in TypeScript and Angular 4

I'm currently working on a comparison of dates in typescript/angular 4. In my scenario, I've stored the system date in a variable called 'today' and the database date in a variable named 'dateToBeCheckOut'. My goal was to filt ...

Tips for changing a created Word file with Docxtemplater into a PDF format

Hey there! I am currently in the process of building a website with Angular.js and have successfully managed to generate a word document from user input. Everything was working fine until I encountered an issue. I now need to provide a way for users to pr ...

Jest snapshot tests are not passing due to consistent output caused by ANSI escape codes

After creating custom jest matchers, I decided to test them using snapshot testing. Surprisingly, the tests passed on my local Windows environment but failed in the CI on Linux. Strangely enough, the output for the failing tests was identical. Determined ...

Utilizing Angular 2 for a dynamic Google Map experience with numerous markers

I am currently working on an Angular2 project that involves integrating Google Maps. My goal is to display multiple markers around a specific area on the map. Although I have been able to get the map running, I am facing issues with displaying the markers ...

Attempting to invoke setState on a Component before it has been mounted is not valid - tsx

I've searched through various threads regarding this issue, but none of them provided a solution that worked for me. Encountering the error: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a b ...

What are the steps to code this in Angular14 syntax?

Here's the code snippet: constructor(obj?: any) { this.id = obj && obj.id || null; this.title = obj && obj.title || null; this.description = obj && obj.description || null; this.thumbnailUrl = obj && obj.thumbnailUrl || null; this. ...

"ENUM value is stored in the event target value's handle property

My issue lies with the event.target.value that returns a 'String' value, and I want it to be recognized as an ENUM type. Here is my current code: export enum Permissions { OnlyMe, Everyone, SelectedPerson } ... <FormControl> & ...

Utilizing a class structure to organize express.Router?

I've been playing around with using Express router and classes in Typescript to organize my routes. This is the approach I've taken so far. In the index.ts file, I'm trying to reference the Notes class from the notes.ts file, which has an en ...