Different TypeScript parameters that cannot be used together

Consider the given JavaScript function below:

function x({foo, fooId, bar, barId}) {}

I am looking to refactor this function into TypeScript in such a way that the caller is required to provide either foo or fooId, but not both. The same rule should apply to bar and barId.

For instance, valid function calls would include

x({foo: "", bar: ""})
and
x({fooId: "", bar: ""})
, while invalid calls like
x({foo: "", fooId: "", bar: ""})
and x({bar: ""}) should be caught by the compiler.

Is it achievable through the TypeScript type system, and if so, how can it be implemented?

Answer №1

If you're searching for the specific type, here it is:

type XArgs = 
 { foo: any; fooId?: never; bar: any; barId?: never; } |
 { foo: any; fooId?: never; barId: any; bar?: never; } | 
 { fooId: any; foo?: never; bar: any; barId?: never; } | 
 { fooId: any; foo?: never; barId: any; bar?: never; };
      
function x({ foo, fooId, bar, barId }: XArgs) { }

x({ foo: "", bar: "" }); // acceptable
x({ fooId: "", bar: "" }); // acceptable
x({ foo: "", fooId: "", bar: "" }); // error
x({ bar: "" }); // error

Hence, XArgs represents a union with four possible structures. Let's analyze the first one:

{ foo: any; fooId?: never; bar: any; barId?: never }

In this case, both foo and bar are required properties of type any</code. However, <code>fooId and barId are optional properties denoted by ? and being values of type never. Since never lacks any viable value, providing defined fooId or barId properties is impossible. Given that optional properties can be omitted, an optional property of type never becomes essentially restricted. Therefore, in this structure, foo and bar must be included while fooId and barId should not.

The remaining three union members follow similar patterns but with varying acceptable and prohibited properties. Together, these four union members comprising XArgs define all conceivable arguments for x().

This summarizes the response to your query.


The manual creation of the necessary union may become overly tedious, especially with multiple exclusive unions (requiring exactly one element) or various significant property sets involved.

To mitigate this complexity, the compiler can derive XArgs through the following calculation:

type AllKeys<T> = T extends unknown ? keyof T : never
type ExclusiveUnion<T, K extends PropertyKey = AllKeys<T>> = 
  T extends unknown ? (T & { [P in Exclude<K, keyof T>]?: never }) : never;

The AllKeys<T> type utilizes distributive conditional type principles to calculate the key union of each T union member. Thus, AllKeys<{a: 0} | {b: 1}> results in "a" | "b".

On the other hand, the ExclusiveUnion<T> type creates an exclusive version from a union like

{a: string} | {b: number} | {c: boolean}
, explicitly disallowing elements present only in other members. By utilizing AllKeys to access keys from other members, the outcome aligns with
{a: string, b?: never, c?: never} | {a?: never, b: number, c?: never} | {a?: never, b?: never, c: boolean}
.

Note that it generates unions through intersections, making it intricate.

Introducing the Expand<T> recursive conditional type helps consolidate intersections and expand any aliased properties:

type Expand<T> = T extends object ? { [K in keyof T]: Expand<T[K]> } : T;

Subsequently, we formulate XArgs as an intersection of ExclusiveUnions and then apply Expand for clarity:

type XArgs = Expand<
  ExclusiveUnion<{ foo: any } | { fooId: any }> &
  ExclusiveUnion<{ bar: any } | { barId: any }>
>;

This translates to

type XArgs = 
 { foo: any; fooId?: never; bar: any; barId?: never; } |
 { foo: any; fooId?: never; barId: any; bar?: never; } | 
 { fooId: any; foo?: never; bar: any; barId?: never; } | 
 { fooId: any; foo?: never; barId: any; bar?: never; };

Try applying this technique to a more complex type for comparison:

type YArgs = Expand<
  ExclusiveUnion<{ a: 0 } | { b: 1 } | { c: 2 }> &
  ExclusiveUnion<{ x: 9 } | { y: 8 } | { z: 7 }>
>
/* Resultant type YArgs = 
  { a: 0, b?: never, c?: never, x: 9, y?: never, z?: never; } | 
  { a: 0, b?: never, c?: never, y: 8, x?: never, z?: never; } | 
  { a: 0, b?: never, c?: never, z: 7, x?: never, y?: never; } | 
  { b: 1, a?: never, c?: never, x: 9, y?: never, z?: never; } | 
  { b: 1, a?: never, c?: never, y: 8, x?: never, z?: never; } | 
  { b: 1, a?: never, c?: never, z: 7, x?: never, y?: never; } | 
  { c: 2, a?: never, b?: never, x: 9, y?: never, z?: never; } | 
  { c: 2, a?: never, b?: never, y: 8, x?: never, z?: never; } | 
  { c: 2, a?: never, b?: never, z: 7, x?: never, y?: never; } */

Results appear satisfactory!

Review code on Playground

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

Unable to access property value following AJAX call

Here is my code snippet: constructor(props: any) { super(props); this.state = { list: [], }; } public componentWillMount() { this.loadData(); } public loadData = () => { axios.get(someURL) .then((response) = ...

TypeScript conditional return type: effective for single condition but not for multiple conditions

In my code, I have implemented a factory function that generates shapes based on a discriminated union of shape arguments. Here is an example: interface CircleArgs { type: "circle", radius: number }; interface SquareArgs { type: "square" ...

Assign a variable with the value returned by a function

Can you help me with this question I have about validating fields with a function using AbstractControl? errorVar: boolean = false function(c: AbstractControl): {[key: string]: string } | null { // validation if 'test' is true or not goes here ...

Perform a function on Angular 5

In my database, there is a table named settings. I have 2 backend endpoints: one for getting settings and another for updating settings. Now I am working on creating an Angular window to edit this table. I've set up an Angular Service to retrieve va ...

Default exports are not supported in TypeScript

I'm encountering issues with my Laravel + Vite + Vue 3 project. I followed the installation instructions in the documentation and everything works fine when the project is separated from Laravel and Vite. However, I'm facing a problem where TypeS ...

Enter data into the appropriate columns

Within my Angular 6 application, I am creating a table with the following structure: Html: <table> <thead> <tr> <th> Property Details &nbsp; &nbsp; &nbsp; &nbsp; ...

How can I retrieve the SID received in a different tab using MSAL.js?

I have successfully integrated MSAL into a client-side library, and things are going smoothly so far. My next goal is to enable Single Sign-On (SSO) by following the instructions provided in the documentation at https://learn.microsoft.com/en-us/azure/act ...

Instructions for activating the "Navigate to Declaration" feature in TypeScript for JSON files using i18next

Currently, I am actively engaged in a project that involves the use of i18next with react and typescript. In this project, translation keys are defined within .json files. However, a notable drawback of transitioning to json for the translation files is l ...

Why is my input field value not getting set by Angular's patchValue function

I've been attempting to populate an input field using the form group with patchValue(), but for some reason, the input remains empty. Here's a snippet of my code... component.html <form [formGroup]="createStoreForm" (ngSubmit)="createStor ...

Display a React functional component

Greetings, friends! I recently created a React app using functional components and now I am looking to print a specific page within the app. Each page is its own functional component, so I was wondering if it's possible to print a component individual ...

Having trouble launching the freshly developed Angular app

I'm encountering an issue with my newly created app - I can't seem to launch it. Error: The loader “C:/C#/Angular/my-app/src/app/app.component.css” is not providing a string as expected. https://i.sstatic.net/6Xjwd.png https://i.sstatic.ne ...

Disable the yellow curly error lines in Visual Studio Code

Currently, I am utilizing VSCode with ESlint for Typescript development. I'm curious about how to turn off or remove the yellow curled error lines in my code editor, like the ones displayed in this example image: https://i.stack.imgur.com/Zdtza.png M ...

GraphQL query result does not contain the specified property

Utilizing a GraphQL query in my React component to fetch data looks like this: const { data, error, loading } = useGetEmployeeQuery({ variables: { id: "a34c0d11-f51d-4a9b-ac7fd-bfb7cbffa" } }); When attempting to destructure the data, an error ...

What could be causing this error for my NPM module in a .NET Core project using Typescript?

My Typescript configuration seems to be causing some issues, even though everything works fine without TS. Could the problem lie in my .d.ts file? And do I really need it for webpack? I have a basic NPM module: index.js: var MyMathTS = function(a, b){ ...

NativeScript encountered an error while trying to locate the module 'ui/sidedrawer' for the specified element 'Sidedrawer'

Currently, I am in the process of developing a simple side-drawer menu for my NativeScript application by following this helpful tutorial. I have successfully implemented it on a single page using the given code below. starting_point.xml: <Page xmlns ...

An error has occurred in the Next.js App: createContext function is not defined

While developing a Next.js application, I keep encountering the same error message TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_0__.createContext) is not a function every time I try to run my app using npm run dev. This issue arises when attempting to co ...

Executing the outer function from within the inner function of a different outer function

Imagine this scenario: function firstFunction() { console.log("This is the first function") } secondFunction() { thirdFunction() { //call firstFunction inside thirdFunction } } What is the way to invoke firstFunction from thirdFunction? ...

Tips for integrating a variety of components onto a single webpage

Exploring the functionality of Angular, I am looking to include multiple components on a single page. How can this be achieved effectively in Angular? I envision each div representing a distinct component and view, with all components residing in separate ...

Is there a way to make the Sweetalert2 alert appear just one time?

Here's my question - can sweetalert2 be set to only appear once per page? So that it remembers if it has already shown the alert. Swal.fire({ title: 'Do you want to save the changes?', showDenyButton: true, showCancelButton: true, ...

The Angular project was functioning properly when tested locally, but encountered an error in the Quill Editor during the building process

I have encountered an issue when deploying my Angular 8 + Quill project. Everything works fine locally with 'ng serve', but upon deployment, I am facing the following error. Despite trying various solutions like updating Angular or deleting &apos ...