Utilizing TypeScript's conditional return type with an object as a parameter, and incorporating default values

Is it possible to create a function where the return type is determined by a string, with some additional complexities involved? I'm looking to achieve the following:

  • The parameter is contained within an object
  • The parameter is optional
  • The object itself is also optional

I want to know if this can be achieved. Here's a simplified version of what I have in mind:

interface NumberWrapper {
  type: "my_number";
  value: number;
}

const x1: number        = fn(1, { returnType: "bare" });
const x2: NumberWrapper = fn(1, { returnType: "wrapped" });
const x3: void          = fn(1, { returnType: "none" });
const x4: number        = fn(1, {});
const x5: number        = fn(1);

In the examples above, different ways are shown on how I intend to call my function fn() and have TypeScript correctly deduce the return type.

For x1, x2, x3, the function is called with a specified value for returnType.

For x4, no value is provided for returnType, and I aim for it to default to "bare". Similarly, for x5, when no object is supplied at all, it should act as if returnType is "bare".

(You might be questioning why I'm placing returnType in an object when it's just one parameter. In my real-world scenario, there are other parameters in the object.)

So far, I've managed to make x1, x2, and x3 work, but not x4 or x5. This is what I currently have:

type ReturnMap<T extends "bare" | "wrapped" | "none"> = T extends "bare"
  ? number
  : T extends "wrapped"
  ? NumberWrapper
  : void;

function fn<T extends "bare" | "wrapped" | "none">(
  x: number,
  { returnType }: { returnType: T }
): ReturnMap<T> {
  if (returnType === "bare") {
    return x as ReturnMap<T>;
  } else if (returnType === "wrapped") {
    return { type: "my_number", value: x } as ReturnMap<T>;
  } else {
    return undefined as ReturnMap<T>;
  }
}

An issue I am facing is that each return statement ends with return x as ReturnMap<T>. I would prefer to avoid this as the as may compromise some type safety.

However, the main challenge lies in making this work for x4 and x5. I have attempted various approaches using default values, but have not succeeded so far.

Answer №1

One way to quickly resolve the issue is by setting the generic type to encompass the entire argument rather than just the returnType attribute. This approach involves a longer chain of conditional operators to achieve the desired types, although it still necessitates the use of unsightly as assertions.

type Argument = undefined | { returnType?: "bare" | "wrapped" | "none" };
type ReturnTypeMap<T extends Argument> =
  T extends { returnType: 'wrapped' }
    ? NumberWrapper
    : T extends { returnType: 'none' }
      ? void
      : number;

function customFunction<T extends Argument>(
  value: number,
  argument?: T
): ReturnTypeMap<T> {
  if (!argument || !('returnType' in argument) || argument.returnType === 'bare') {
    return value as ReturnTypeMap<T>;
  } else if (argument.returnType === 'wrapped') {
    return { type: "my_number", value: value } as ReturnTypeMap<T>;
  } else {
    return undefined as ReturnTypeMap<T>;
  }
}

const result1: number = customFunction(1, { returnType: "bare" });
const result2: NumberWrapper = customFunction(1, { returnType: "wrapped" });
const result3: void = customFunction(1, { returnType: "none" });
const result4: number = customFunction(1, {});
const result5: number = customFunction(1);

Answer №2

Allow me to share with you an alternative approach for writing your fn() function:

interface ReturnMap {
  bare: number,
  wrapped: NumberWrapper,
  none: void
}

function fn<K extends keyof ReturnMap = "bare">(
  x: number,
  { returnType = "bare" as K }: { returnType?: K } = { returnType: "bare" as K }
): ReturnMap[K] {
  return {
    get bare() { return x },
    get wrapped() { return { type: "my_number" as const, value: x } },
    get none() { return undefined }
  }[returnType]

}

Below are a few explanations:


The issue with your "as ReturnMap<T>" statement arises from the fact that the compiler struggles to determine if your implementation aligns with the return type. The type ReturnMap<T> is a conditional type hinging on a generic type parameter T. In such scenarios, the compiler encounters uncertainty due to its inability to interpret T, resulting in it not comprehending which ReturnMap<T> version to use. While your implementation assesses arg, the compiler cannot utilize these assessments to narrow down the type parameter T. This is a recognized limitation of TypeScript; refer to microsoft/TypeScript#33912 for additional details.

I would suggest restructuring the code into a format that the compiler can grasp, such as leveraging object indexing with a generic key.

This is why ReturnMap serves as an authentic mapping type containing keys corresponding to your returnType values, and whose property values indicate the respective return types. Consequently, fn() can be flexible through K extends keyof ReturnMap, enabling you to retrieve ReturnMap[K] by accessing the returnType property within an object of type ReturnMap.

Note that I've opted for utilizing getter methods to elude the necessity for pre-computing outcomes for every feasible input within the ReturnMap object.


In addressing the default concerns associated with the x4 and x5 cases, the desired behavior can be replicated utilizing JavaScript by combining both a function parameter default and a destructuring assignment default:

function fn(x, {returnType = "bare"} = {returnType: "bare"}) {}

This setup yields the exact runtime behavior you desire. However, when applying typings, you may need to incorporate certain type assertions to convince the compiler to accept them. This requirement stems from the less-than-seamless interaction between generics and default parameters in TypeScript. Refer to this question or this one for further insights.

To manage defaults, we assign K a default type of "bare"; hence, in instances where K isn't inferred by the compiler during a call to fn(), it defaults back to "bare". Similarly, we establish the returnType variable's default as "bare" within the implementation via JavaScript's destructuring assignment default and function parameter default...

...yet, the compiler fails to anticipate this duo of defaults. This necessitates the utilization of type assertions. With the declaration "bare" as K, I signify that, in scenarios employing the default for returnType , the default for K will also apply. It remains plausible for someone to craft fn< "none">(1), leading to potential disruptions. Nonetheless, given the low likelihood of this occurrence, we can confidently assume that K equates to "bare" when defaults come into play.


In summary, this method generates the intended outcomes:

const x1: number = fn(1, { returnType: "bare" });
const x2: NumberWrapper = fn(1, { returnType: "wrapped" });
const x3: void = fn(1, { returnType:& quot;"none" });
const x4: number = fn(1, {});
const x5: number = fn(1);

Access the Playground link to code

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

I am attempting to gather user input for an array while also ensuring that duplicate values are being checked

Can someone assist me with the following issue: https://stackblitz.com/edit/duplicates-aas5zs?file=app%2Fapp.component.ts,app%2Fapp.component.html I am having trouble finding duplicate values and displaying them below. Any guidance would be appreciated. I ...

Using GraphQL in React to access a specific field

Here is the code snippet I am working with: interface MutationProps { username: string; Mutation: any; } export const UseCustomMutation: React.FC<MutationProps> | any = (username: any, Mutation: DocumentNode ) => { const [functi ...

Experimenting with a Collection of Items - Jest

I need to conduct a test on an array of objects. During the test coverage analysis of the displayed array, I found that the last object with the key link has certain conditions that are not covered. export const relatedServicesList: IRelatedServiceItem[ ...

Performing optimized searches in Redis

In the process of creating a wallet app, I have incorporated redis for storing the current wallet balance of each user. Recently, I was tasked with finding a method to retrieve the total sum of all users' balances within the application. Since this in ...

Typescript defines types for parameters used in callbacks for an event bus

Encountering a TypeScript error with our custom event bus: TS2345: Argument of type 'unknown' is not assignable to parameter of type 'AccountInfo | undefined'. Type 'unknown The event bus utilizes unknown[] as an argument for ca ...

The observable did not trigger the next() callback

I'm currently working on incorporating a global loading indicator that can be utilized throughout the entire application. I have created an injectable service with show and hide functions: import { Injectable } from '@angular/core'; import ...

Utilize the global theme feature within React Material-UI to create a cohesive

I'm feeling a bit lost when it comes to setting up React Material-UI theme. Even though I've tried to keep it simple, it's not working out for me as expected. Here is the code snippet I have: start.tsx const theme = createMuiTheme({ ...

Module lazily loaded fails to load in Internet Explorer 11

Encountering an issue in my Angular 7 application where two modules, txxxxx module and configuration module, are lazy loaded from the App Routing Module. The problem arises when attempting to navigate to the configuration module, as it throws an error stat ...

The presence of 'eventually' in the Chai Mocha test Promise Property is undefined

I'm having trouble with using Chai Promise test in a Docker environment. Here is a simple function: let funcPromise = (n) => { return new Promise((resolve, reject) =>{ if(n=="a") { resolve("success"); ...

Why is my custom Vuelidate validator not receiving the value from the component where it is being called?

On my registration page, I implemented a custom validator to ensure that the password meets specific criteria such as being at least 12 characters long and containing at least one digit. However, I encountered an issue where the custom validator was not r ...

Angular time-based polling with conditions

My current situation involves polling a rest API every 1 second to get a result: interval(1000) .pipe( startWith(0), switchMap(() => this.itemService.getItems(shopId)) ) .subscribe(response => { console.log(r ...

The React component fails to load due to the discrepancies in the data retrieved from various asynchronous requests

Creating a travel-related form using React with TypeScript. The initial component TravelForm utilizes multiple async-await requests within useEffect hook to update the state of the subsequent component TravelGuideFields However, the values of props a ...

How can we use tsyringe (a dependency injection library) to resolve classes with dependencies?

I seem to be struggling with understanding how TSyringe handles classes with dependencies. To illustrate my issue, I have created a simple example. In my index.tsx file, following the documentation, I import reflect-metadata. When injecting a singleton cl ...

The TypeScript datatype 'string | null' cannot be assigned to the datatype 'string'

Within this excerpt, I've encountered the following error: Type 'string | null' cannot be assigned to type 'string'. Type 'null' cannot be assigned to type 'string'. TS2322 async function FetchSpecificCoinBy ...

Obtaining Input Field Value in Angular Using Code

How can I pass input values to a function in order to trigger an alert? Check out the HTML code below: <div class="container p-5 "> <input #titleInput *ngIf="isClicked" type="text" class="col-4"><br& ...

Change the background color of a span element dynamically

I am currently working on implementing dynamic background coloring for a span tag in my Angular view that displays document types. The code snippet provided is as follows: <mat-card *ngFor="let record of records"> <span class="doc ...

Issue with the drag functionality of Framer Motion carousel causing malfunction

Attempting to create a basic Image Carousel using framer-motion for added functionality. The goal is to incorporate both buttons and drag control for sliding through the images. Currently, it functions properly, but if the slider overshoots on the last im ...

reusable angular elements

I'm facing a situation where I have a search text box within an Angular component that is responsible for searching a list of names. To avoid code duplication across multiple pages, I am looking to refactor this into a reusable component. What would b ...

"What is the significance of the .default property in scss modules when used with typescript

When dealing with scss modules in a TypeScript environment, my modules are saved within a property named default. Button-styles.scss .button { background-color: black; } index.tsx import * as React from 'react'; import * as styles from ' ...

Learn how to implement React Redux using React Hooks and correctly use the useDispatch function while ensuring type-checking

I'm curious about the implementation of Redux with Typescript in a React App. I have set up type-checking on Reducer and I'm using useTypedSelector from react-redux. The only issue I have is with loose type-checking inside the case statements of ...