Using TypeScript, a parameter is required only if another parameter is passed, and this rule applies multiple

I'm working on a concept of a distributed union type where passing one key makes other keys required.

interface BaseArgs {
    title: string
}

interface FuncPagerArgs {
    enablePager: true
    limit: number
    count: number
}

type FuncArgs = (FuncPagerArgs & BaseArgs) | BaseArgs;

function func(args: FuncArgs) {
    if ("enablePager" in args) {
        pager({ limit: args.limit, count: args.count });
    }
}

interface PagerArgs {
    limit: number
    count: number
}

function pager(args: PagerArgs) {

}

func({
    title: "something",
    enablePager: true
})

Link to TypeScript code playground

In my opinion, the provided code should fail validation because I am calling func while passing enablePager without also providing limit or count which are necessary when enablePager is true. In a real-world scenario, I attempted to implement this pattern for 3 or 4 different feature booleans, each of which would require additional fields in the contract. Regrettably, I encountered difficulties even with the first feature boolean, let alone multiple ones.

Answer №1

The problem we are encountering is due to the lack of proper distinction between elements in the union. There is an overlap between two members

(FuncPagerArgs & BaseArgs) | BaseArgs
, with the overlap being equal to BaseArgs. It is crucial to understand the behavior of function argument types in TypeScript:

When comparing function parameter types, assignment succeeds if either the source parameter can be assigned to the target parameter, or vice versa.

This means that if our type can be assigned to the desired type, or vice versa, the argument can be passed. Consider the following test:

type ArgType = {
    title: "something",
    enablePager: true
}
type IsAssignable = ArgType extends FuncArgs ? true : false; // true, can be passed

If the type you are passing is assignable to the desired type, TypeScript allows it to be passed. This flexibility is a deliberate design decision to accommodate various behaviors, as mentioned in the linked TypeScript documentation. However, there may still be runtime errors within the code. To address this issue, we need to create a discriminated union without any overlaps.

interface BaseArgs {
    title: string
}

interface OnlyTitle extends BaseArgs {
    kind: 'TITLE'
}

interface FuncPagerArgs extends BaseArgs {
    kind: 'PAGER'
    enablePager: true
    limit: number
    count: number
}

type FuncArgs = OnlyTitle | FuncPagerArgs;

function func(args: FuncArgs) {
    if (args.kind === 'PAGER') {
        pager({ limit: args.limit, count: args.count });
    }
}

interface PagerArgs {
    limit: number
    count: number
}

function pager(args: PagerArgs) {

}

func({
    kind: "TITLE",
    enablePager: true
}) // error 

func({
    kind: "TITLE",
    title: 'title'
}) // correct 

I introduced a discriminant property called kind, which ensures that each member of the union is distinct without any overlap. Now, you can only use one variant at a time, preventing any overlap in types.

Edit after comment

In response to a comment requesting to combine all options together, while ensuring that when something is enabled, all related options should be included, we can achieve this by grouping and joining the options. Here's an example:

interface Base {
    title: string
}
interface Pager  {
  pager?: {
    limit: number
    count: number
  }  
}

interface Sort {
  sorter?: {
    columns: string[] // example property
  }  
}

type Options = Pager & Sort & Base;

function func(args: Options) {
    if (args.pager) {
        pager({ limit: args.pager.limit, count: args.pager.count });
    }
    if (args.sorter) {
       sorter(args.sorter.columns)
    }
}

func({
  title: 'title',
  pager: {count: 2, limit: 10} // all fields its ok
})


func({
  title: 'title',
  pager: {count: 2} // error limit is missing
})

Now, we can have both pager and sorter together. When using pager, all related options must be provided as they are required.

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

Implementing an Asynchronous Limited Queue in JavaScript/TypeScript with async/await

Trying to grasp the concept of async/await, I am faced with the following code snippet: class AsyncQueue<T> { queue = Array<T>() maxSize = 1 async enqueue(x: T) { if (this.queue.length > this.maxSize) { // B ...

The 'HTMLDivElement' type does not include the property 'prepend' in Typescript

When working with a typescript method, the following code snippet is used: some_existing_div.prepend(some_new_div) This may result in an error message: [ts] Property 'prepend' does not exist on type 'HTMLDivElement'. However, despi ...

Guide to inserting an Angular routerLink within a cell in ag-Grid

When attempting to display a link on a basic HTML page, the code looks like this: <a [routerLink]="['/leverance/detail', 13]">A new link</a> However, when trying to render it within an ag-Grid, the approach is as follows: src\ ...

Troubleshooting Angular4 and TypeScript Compile Error TS2453: A Comprehensive

I'm facing an issue in my Angular4 project build process which I find quite perplexing. The error seems to be related to the import of certain components such as Response, Headers, and Http from @angular. Whenever I attempt to build my project, it thr ...

When importing a module, the function in the ts file may not be recognized or located

While attempting to create a VSTS (Azure Devops) Extension, I encountered a perplexing issue. Within my HTML page, I have a button element with an onclick listener: <!DOCTYPE html> <head> <script type="text/javascript"> VS ...

Proper method for inserting a value into a string array in a React application using TypeScript

How can I properly add elements to a string array in react? I encountered an error message: Type '(string | string[])[]' is not assignable to type 'string[]' You can view the code on this playground link : Here Could it be that I&apos ...

Displaying a collection of objects in HTML by iterating through an array

As someone new to coding, I am eager to tackle the following challenge: I have designed 3 distinct classes. The primary class is the Place class, followed by a restaurant class and an events class. Both the restaurant class and events class inherit core p ...

A guide on assigning specific (x, y) coordinates to individual IDs within the tree structure

When attempting to calculate the positions of each ID in order to arrange them hierarchically on the canvas, I encounter some challenges. Whether it's organizing them into a tree structure or multiple trees resembling a forest, one restriction is that ...

Node.js: The choice between returning the original Promise or creating a new Promise instance

Currently, I am in the process of refactoring a codebase that heavily relies on Promises. One approach I am considering is replacing the new Promise declaration with simply returning the initial Promise instead. However, I want to ensure that I am correctl ...

Restrict the parameter type using a type predicate

How can I effectively narrow types based on the value of a single field in TypeScript? It seems that using type predicates may not be working as expected to narrow down the types of other parameters within a type. Is there a way to ensure correct type na ...

What specific element is being targeted when a directive injects a ViewContainerRef?

What specific element is associated with a ViewContainerRef when injected into a directive? Take this scenario, where we have the following template: template `<div><span vcdirective></span></div>` Now, if the constructor for the ...

Retrieve various data types through a function's optional parameter using TypeScript

Creating a custom usePromise function I have a requirement to create my own usePromise implementation. // if with filterKey(e.g `K=list`), fileNodes's type should be `FileNode` (i.e. T[K]) const [fileNodes, isOk] = usePromise( () => { ...

Retrieving the location.host parameter within NgModule

I am currently working on integrating Angular Adal for authenticating my application's admin interface with Azure AD. However, I have encountered a challenge with the redirectUri setting. My goal is to dynamically retrieve the current app's host ...

Evaluation of button display based on certain conditions

I currently have two different render functions that display certain elements based on specific conditions. The first render function looks like this: private render(): JSX.Element { return ( <div> {this.props.x && this.state.y ...

Leverage the state from a Context within a Class-based component

I have a Codepen showcasing my current issue. I want to utilize the class component so that I can invoke the forward function from parentComponents via ref. However, I am struggling with how to manipulate the context where the application's current st ...

Splitting a string in Typescript based on regex group that identifies digits from the end

Looking to separate a string in a specific format - text = "a bunch of words 22 minutes ago some additional text". Only interested in the portion before the digits, like "a bunch of words". The string may contain 'minute', & ...

What is the best way to output data to the console from an observable subscription?

I was working with a simple function that is part of a service and returns an observable containing an object: private someData; getDataStream(): Observable<any> { return Observable.of(this.someData); } I decided to subscribe to this funct ...

Steps to specify a prefix for declaring a string data type:

Can we define a string type that must start with a specific prefix? For instance, like this: type Path = 'site/' + string; let path1: Path = 'site/index'; // Valid let path2: Path = 'app/index'; // Invalid ...

How can I create an Array of objects that implement an interface? Here's an example: myData: Array<myInterface> = []; However, I encountered an issue where the error message "Property 'xxxxxx' does not exist on type 'myInterface[]'" appears

Currently, I am in the process of defining an interface for an array of objects. My goal is to set the initial value within the component as an empty array. Within a file, I have created the following interface: export interface myInterface{ "pictur ...

How can the file system module (fs) be utilized in Angular 7?

Hello, I am currently working with Angular 7. I recently attempted to utilize the fs module in Typescript to open a directory. However, I encountered an error message stating: "Module not found: Error: Can't resolve fs". Despite having types@node and ...