TypeScript mandates the inclusion of either one parameter or the other, without the possibility of having neither

Consider the following type:

export interface Opts {
  paths?: string | Array<string>,
  path?: string | Array<string>
}

The requirement is that the user must provide either 'paths' or 'path', but it is not mandatory to pass both. Currently, the issue is that this code compiles without errors:

export const foo = (o: Opts) => {};
foo({});

Is there a way in TypeScript to allow for two or more optional parameters where at least one is required?

Answer №1

If needed, you can utilize the following syntax:

export type Choices = { choice: string | Array<string> } | { options: string | Array<string> }

For better clarity, consider rephrasing as follows:

type StringOrArr = string | Array<string>;

type ChoiceOpts  = { choice : StringOrArr };
type OptionsOpts = { options: StringOrArr };

export type Choices = ChoiceOpts | OptionsOpts;

Answer №2

If you have an existing interface defined and want to prevent duplicate declarations, one approach is to create a conditional type that takes a type and returns a union with each type containing a single field (along with a record of never values for any additional fields to disallow specifying extra fields).

export interface Options {
    paths?: string | Array<string>,
    path?: string | Array<string>
}

type SingleField<T, Key extends keyof T = keyof T> =
    Key extends keyof T ? { [Prop in Key]-?:T[Key] } & Partial<Record<Exclude<keyof T, Key>, never>>: never
export const handleOptions = (o: SingleField<Options>) => {};
handleOptions({ path : '' });
handleOptions({ paths: '' });
handleOptions({ path : '', paths:'' }); // error
handleOptions({}) // error

Additional Information:

A closer look at the type manipulation technique employed here. We utilize the distributive property of conditional types to essentially iterate over all keys of the T type. The distributive property requires an extra type parameter to function, so we introduce Key for this purpose with a default value of all keys since we aim to extract all keys from type T.

Our goal is to derive individual mapped types for each key of the original type, resulting in a union of these mapped types, each encapsulating just one key. This process removes optionality from the property (indicated by -?, detailed here) while maintaining the same data type as the corresponding property in T (T[Key]).

The final aspect worth elaborating on is

Partial<Record<Exclude<keyof T, Key>, never>>
. Due to how object literals undergo excess property checks, it's possible to assign any field of the union to an object key within an object literal. For instance, a union like
{ path: string | Array<string> } | { paths: string | Array<string> }
permits employing the object literal
{ path: "", paths: ""}</code, which isn't ideal. To remedy this issue, we mandate that if other properties of <code>T
(beyond Key, denoted by Exclude<keyof T, Key>) are included in the object literal of any union member, they must be of type never (hence using
Record<Exclude<keyof T, Key>, never>>
). By partially modifying the preceding record, explicit specification of never for every member is avoided.

Answer №3

This is a functional example.

It allows for the use of a generic type T, specifically with a value of string.

The generic type OneOrMore can be either a single instance of T or an array containing multiple instances of T.

Your input object type Opts is structured to have either a property path containing OneOrMore<T>, or a property paths also containing OneOrMore<T>. There is no other acceptable option provided in the definition.

type OneOrMore<T> = T | T[];

export type Opts<T> = { path: OneOrMore<T> } | { paths: OneOrMore<T> } | never;

export const exampleFunction = (o: Opts<string>) => {};

exampleFunction({});

An issue arises with the usage of {} as an argument.

Answer №4

export type Opts={paths: string | Array<string>,  path?: undefined}  |
                 {paths?: undefined,  path: string | Array<string>}

In my opinion, this code is quite clear and easy to comprehend.

Answer №5

Searching for a unique union type that suits your needs?

A proposal was presented in the past, but unfortunately, it was rejected. You can read more here.

The suggested solutions didn't resonate with me as I prefer straightforward types over complex ones.

Have you considered using function overloading? It worked for me in a similar scenario.

interface Option1 {
  paths: string | string[];
}

interface Option2 {
  path: string | string[];
}

function foo(o: Option1): void;
function foo(o: Option2): void;
function foo(o: any): any {}

foo({ path: './' });
foo({ paths: '../' });
// The following two lines result in an error: No overload matches this call.
foo({ paths: './', path: '../' });
foo({})

If you prefer using arrow functions, you can adjust the code like this:

interface Option1 {
  paths: string | string[];
}

interface Option2 {
  path: string | string[];
}

interface fooOverload {
  (o: Option1): void;
  (o: Option2): void;
}

const foo: fooOverload = (o: any) => {};

foo({ path: '2' });
foo({ paths: '2' });
// The following two lines will show an error: No overload matches this call.
foo({ paths: '', path: ''});
foo({});

I hope this solution works for you!

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

What is the most effective way to move specific data from one page to another in Angular/Typescript?

Welcome to my Main Page! https://i.stack.imgur.com/m9ASF.png This is where I want to start my journey. https://i.stack.imgur.com/E8pAW.png My goal is to click the Last 1 Day button to redirect to another page with the date filter and ItemId values already ...

Why did the homepage load faster than the app.component.ts file?

I am facing an issue where the homepage is loading before app.component.ts, causing problems with certain providers not working properly due to import processes not being fully completed. Despite trying to lazy load the homepage, the console.log still sho ...

Using WebdriverIO with Angular to create end-to-end tests in TypeScript that involve importing classes leads to an error stating "Cannot use import statement outside a module."

I am facing an issue while trying to set up a suite of end-to-end tests using wdio. Some of the tests utilize utility classes written in TypeScript. During the compilation of the test, I encountered the following error: Spec file(s): D:\TEMP\xx& ...

"Angular encountered an error while attempting to access the property 'data' from an undefined

I'm encountering an issue when trying to retrieve data from a JSON file. Upon clicking a button to display the data in a textarea, the first click does not yield any result, but subsequent clicks work as expected. The program functions by fetching an ...

Tips for establishing communication between a server-side and client-side component in Next.js

I am facing a challenge in creating an interactive component for language switching on my website and storing the selected language in cookies. The issue arises from the fact that Next.js does not support reactive hooks for server-side components, which ar ...

Can we handle optional properties that are contingent on a boolean in the type?

My current scenario involves a server response containing a boolean indicating success and optional data or error information. type ServerResponse = { success: boolean; data?: { [key: string]: string }; err?: { code: number, message: string }; } Dea ...

The content security policy is preventing a connection to the signalr hub

Currently, I am developing an application using electron that incorporates React and Typescript. One of the features I am integrating is a SignalR hub for chat functionality. However, when attempting to connect to my SignalR server, I encounter the followi ...

Turn TypeScript - Modify type properties to reflect types of their descendants

I am currently working on creating a type that will modify a generic type based on its children. To provide some clarity, I have created a simplified example below: Original type type FormFields = { username: { type: string, ...

Angular8: Adjusting Activity Status After Leaving Page

When performing activities like upload, download, delete, and edit, I display statuses such as 'upload started' or 'upload completed'. This works perfectly when staying on the same page. However, there are instances where a user may nav ...

Convert JSON data to an array using Observable

My current task involves parsing JSON Data from an API and organizing it into separate arrays. The data is structured as follows: [ {"MONTH":9,"YEAR":2015,"SUMAMT":0}, {"MONTH":10,"YEAR":2015,"SUMAMT":11446.5}, {"MONTH":11,"YEAR":2015,"SUMAMT":5392 ...

Is there a source where I can locate type definitions for Promise objects?

In the process of creating a straightforward class called Primrose, I am extending the global Promise object in order to include additional methods like resolve and reject. export class Primrose<Resolution> extends Promise<Resolution>{ priv ...

Having trouble importing a TypeScript module from the global node_modules directory

I have a library folder located in the global node modules directory with a file named index.ts inside the library/src folder //inside index.ts export * from './components/button.component'; Now I am trying to import this into my angular-cli ap ...

Challenges Encountered when Making Multiple API Requests

I've encountered a puzzling issue with an ngrx effect I developed to fetch data from multiple API calls. Strangely, while some calls return data successfully, others are returning null for no apparent reason. Effect: @Effect() loadMoveList$: Obse ...

The Order ID field in the Serenity-Platform's Order Details tab is not registering orders

I've been working on replicating the functionality of Orders-Order detail in my own project. https://i.stack.imgur.com/Bt47B.png My custom module is called Contract and Contract Line item, which I'm using to achieve this. https://i.stack.imgur ...

Can you explain the correct method for assigning types when destructuring the `callbackFn.currentValue` in conjunction with the `.reduce()` method? Thank you

I'm working with an array of arrays, for example: const input = [['A', 'X'], ['B', 'Y'],...]; In addition to that, I have two enums: enum MyMove { Rock = 'X', Paper = 'Y', Scis ...

I am facing an issue with the Angular2 Modal Form where it only displays the data but does

Hey there, I recently started diving into Angular and I'm loving the learning process. Currently, I've managed to successfully load a form into my Modal when clicking on "viewDetails". However, as soon as I modify the Form from <form ngNoFo ...

Is it possible to apply a formatting filter or pipe dynamically within an *ngFor loop in Angular (versions 2 and 4

Here is the data Object within my component sampleData=[ { "value": "sample value with no formatter", "formatter": null, }, { "value": "1234.5678", "formatter": "number:'3.5-5'", }, { "value": "1.3495", "formatt ...

What could be triggering the "slice is not defined" error in this TypeScript and Vue 3 application?

I have been developing a Single Page Application using Vue 3, TypeScript, and the The Movie Database (TMDB) API. In my src\components\MovieDetails.vue file, I have implemented the following: <template> <div class="row"> ...

Optimizing Angular for search engines: step-by-step guide

Regarding Angular SEO, I have a question about setting meta tags in the constructors of .ts files. I have implemented the following code: //To set the page title this.titleServ.setTitle("PAGE TITLE") //To set the meta description this.meta.addTag ...

Creating an array with varying types for the first element and remaining elements

Trying to properly define an array structure like this: type HeadItem = { type: "Head" } type RestItem = { type: "Rest" } const myArray = [{ type: "Head" }, { type: "Rest" }, { type: "Rest" }] The number of rest elements can vary, but the first element ...