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

Just made the switch to Mongoose 5.12 and hit a snag - unable to use findOneAndUpdate with the $push operator

After upgrading to Mongoose 5.12 from 5.11 and incorporating Typescript, I encountered an issue with my schema: const MyFileSchema = new Schema<IMyFile>({ objectID: { type: String, required: true }, attachments: { type: Array, required: false ...

Utilizing Typescript to implement an interface's properties

After declaring an interface as shown below interface Base { required: string; } I proceeded to implement the interface in a class like this class MyClass implements Base{ method(): void { console.log(this.required); } } However, I ...

The jquery methods might encounter an issue with an undefined object

I have been attempting to implement a function that triggers when the user reaches the bottom of the window, but TypeScript is flagging errors and mentioning potential undefined objects related to window.scrollTop(), height(), and document.height(). Even ...

Angular Iterate Array Forms

Is there a way to iterate my rows based on user input? For example, if the user enters 2 values, I need to display 2 rows. How can I achieve this using FormArray? setCount(count:any){ console.log(count); this.count = count; this.countArray = Ar ...

Angular 2 Mixup: Using Leaflet and Google Maps with Incorrect Tile Order

I am encountering an issue while working on Leaflet and Google within Angular 2. The problem lies in the Tilemill tiles not rendering properly as they are displaying in a strange order. Here is a screenshot of the current state: https://i.stack.imgur.com/ ...

Experimenting with Nuxtjs application using AVA and TypeScript

I'm in the process of developing a Nuxt application using TypeScript and intend to conduct unit testing with AVA. Nonetheless, upon attempting to run a test, I encounter the following error message: ✖ No test files were found The @nuxt/typescrip ...

Issue: The --outFile flag only supports 'amd' and 'system' modules

Encountering an issue while trying to compile an Angular project in Visual Studio Code using ng build and then serving it with ng serve Unfortunately, faced the following error message in both cases: The error 'Only 'amd' and 'syste ...

TypeORM find query is returning a data type that does not match the defined entity type

In my infrastructure module, I am using the code snippet below: import { Student } from "core" import { Repository } from "./Repository" import { Database } from "../../db" export class UserRepository<Student> extends Re ...

Troubleshooting issue with Vue3 - TS Custom State Management

Issue: I am facing a challenge in transferring data between two separate components - the main component and another component. Despite attempting to implement reactive State Management based on Vue documentation, the object's value remains unchanged ...

How is it possible for a TypeScript function to return something when its return type is void?

I find the book Learning JavaScript to be confusing. It delivers conflicting information regarding the use of void as a return type in functions. const foo = (s: string): void => { return 1; // ERROR } At first, it states that if a function has a re ...

Unable to choose Typescript as a programming language on the VSCode platform

Recently, I encountered an issue while using Visual Studio Code with TypeScript. Even though TypeScript is installed globally, it is not showing up in the list of file languages for syntax highlighting. Despite trying various troubleshooting methods such a ...

Utilizing Node.js Functions for Sharing Database Queries

When it comes to sharing DB query code among multiple Node.js Express controller methods, finding the best practice can be challenging. Many samples available online don't delve deeply into this specific aspect. For instance, let's consider a ge ...

Strange Typescript Issue: Dependency Imports Not Recognized as Top-Level Module

Attempting to move a custom token from one account to another by following this detailed tutorial. Facing an issue with four errors showing up for all imports from the @solana/spl-token package. Tried removing the node-modules folder and reinstalling npm ...

Concatenate all sub-items within a JSON object

I have 2 Objects like this : [ { _id: ObjectId("62990f96345ef9001d9f2dfe"), deletedAt: null, expiredAt: ISODate("2022-06-05T19:29:26.746Z"), dataBarang: [ { vendor: ObjectId("6215dd91139c99003fe4c7cd ...

Testing Angular2 / TypeScript HTTPService without Mocking: A Guide

import {Injectable} from '@angular/core'; import {Http} from '@angular/http'; @Injectable() export class HttpService { result: any; constructor(private http:Http) { } public postRequest(){ return this.http.get('h ...

Does nestjs support typescript version 3.x?

Currently embarking on a new project using Nestjs. I noticed in one of its sample projects, the version of Typescript being used is 2.8. However, the latest version of Typescript is now 3.2. Can anyone confirm if Nest.js supports version 3.x of Typescrip ...

Traverse through an array of objects with unspecified length and undefined key names

Consider the following object arrays: 1. [{id:'1', code:'somecode', desc:'this is the description'}, {...}, {...}] 2. [{fname:'name', lname:'last name', address:'my address', email:'<a h ...

Utilizing Angular2 to scan barcodes

Im looking to create an application in asp.net 5 / Angular2 and I am encountering an issue with scanning barcodes. This is the TypeScript code for my component: @Component({ selector: 'barcode-scanner', templateUrl: 'app/scan.html& ...

Aframe failing to display image when using local path in Angular with AR.js

I am attempting to load an image from a local path within my Angular app component.html file. Below is the code snippet: <a-scene embedded arjs> <a-assets> <img id="test_img" src="/mnt/r/flipkart/proj/src/app ...

Tips for including and excluding personalized Chips from input

Just started learning React/typescript, any assistance would be greatly appreciated Custom Chip (CC.chip) is a specialized Chip UI component that can be utilized as demonstrated below. const [isOpen, setIsOpen] = useState(true); const onClose = Re ...