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

How to utilize FileReader for parsing a JSON document?

I'm currently facing an issue while attempting to read and copy a JSON file uploaded by the user into an array. When using .readAsText(), the returned data includes string formatting elements like \" and \n. Is there a way to utilize FileRe ...

Issue with Angular Reactive form: Checkbox checked property not binding correctly when the page initially loads

Looking to achieve Two-way data binding of Checkbox in Angular Reactive forms. After checking the checkbox, I am updating the 'isdateChkd' variable and storing it in the state. Despite the variable being set to TRUE, the checkbox does not get aut ...

Angular 2 component downgrade issue: What causes the error when constructor parameters are involved? (SystemJS) Unable to resolve all parameters for (?)

Consider this line as an example: constructor(private elementRef: ElementRef, private zone: NgZone) {} In order for the downgrade to be successful without any errors, I must remove the parameters from the constructor. Otherwise, I encounter the follo ...

Having trouble getting a local npm installation to work from a specific file path?

After following the instructions from this helpful link to install an npm package through a file path, I encountered an error when attempting to use it: Cannot find module '<module_name>' or its corresponding type declaration Are there an ...

Discover the combined type of values from a const enum in Typescript

Within my project, some forms are specified by the backend as a JSON object and then processed in a module of the application. The field type is determined by a specific attribute (fieldType) included for each field; all other options vary based on this ty ...

Typescript's Integrated Compatibility of Types

One important concept I need to convey is that if one of these fields exists, then the other must also exist. When these two fields are peers within the same scope, it can be challenging to clearly communicate this connection. Consider the example of defi ...

Are my Angular CLI animations not working due to a version compatibility issue?

I've been working on a project that had Angular set up when I started. However, the animations are not functioning correctly. The mat input placeholder doesn't disappear when typing, and the mat-select drop-down is not working. Here is my packag ...

Determine the implicit type of the assigned function, while also constraining the return type to be a subtype of a predefined

When writing multiple functions for server requests, I have encountered a dilemma with TypeScript. Each function must return a type that extends a specific predefined known type, but I also want TypeScript to infer the most accurate return type possible. ...

Do Not Activate the Android App with Deeplink in Ionic3

I'm currently using the ionic-plugin-deeplinks to enable deep linking within my app. Here are the steps I followed: $ ionic cordova plugin add ionic-plugin-deeplinks --variable URL_SCHEME=myapp --variable DEEPLINK_SCHEME=https --variable DEEPLINK_HOS ...

Switch between classes when hovering over / exiting ngFor elements

Displayed below is an element created using ngFor <span *ngFor="let picture of pictures; let i = index"> <a target="_blank" href="{{picture.image}}" class="thumbnail-display image-overlay"> <span class="overlay-icon hide"> ...

The datatype 'string' cannot be assigned to the datatype '(token: string) => void'

Within my Angular2 application, I have a class that includes several properties which will be assigned values within components. @Injectable() export class Globals { private token: string; private authorization: string; private roleUser: boole ...

Customize time formatting in Angular to accommodate localized time formats

I am utilizing Angular's localization service version 8.3.28 to support English, Spanish (Mexico) with code es-MX, and Spanish (Spain) with code es-SP While using the date pipe: {{ foo.Date | date: 'shortDate' }} The dates are changing to ...

Error Encountered in Cypress: "Tried to wrap warn but it is already wrapped"

Objective: Utilize Cypress and Typescript to test for warnings and errors on the console. Error Encounter: An attempt was made to wrap warn, which is already wrapped. Snippet: describe.only("Unauthenticated User", () => { it("No C ...

Steps to show the chosen index value in an alert pop-up using Ionic 2 framework

I'm in the process of trying to showcase a selected index value within an Ionic 2 alert box. However, I'm struggling to find the correct method to display it in the Ionic prompt. This pertains to the home.ts import { Component } from '@ang ...

Using `this` within an object declaration

I am encountering an issue with the following code snippet: const myObj = { reply(text: string, options?: Bot.SendMessageOptions) { return bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id, ...options }) ...

Tips for eliminating nested switchMaps with early returns

In my project, I have implemented 3 different endpoints that return upcoming, current, and past events. The requirement is to display only the event that is the farthest in the future without making unnecessary calls to all endpoints at once. To achieve th ...

What causes Next.js to struggle with recognizing TypeScript code in .tsx and .ts files?

Webpages lacking a declared interface load correctly https://i.stack.imgur.com/DJZhy.png https://i.stack.imgur.com/r1XhE.png https://i.stack.imgur.com/zXLqz.png https://i.stack.imgur.com/Z1P3o.png ...

Securely import TypeScript modules from file paths that are dynamically determined during execution

Imagine you have a structure of TypeScript code and assets stored at a specific URL, like between a CDN and a debug location. You want to import the main module and ensure the rest of the structure is imported correctly only when needed, without repeating ...

Unpacking JSON Objects in Typescript: Working with Private Variables

I have a TypeScript model object called user export class User { constructor( private _name: string, private _email: string ) {} public get name():string { return this._name; } public set name(value:string) { this._name = value; } g ...

Passing a boolean value from the parent Stepper component to a child step in Angular

I am facing an issue with passing the boolean value isNewProposal from a parent Angular Material stepper component to its children steps. Despite using @Input(), the boolean remains undefined in the child component, even after being assigned a value (true ...