Determine the return value of a function based on a specific conditional parameter

Is it possible for a function with a parameter of a conditional type to return conditionally based on that parameter?

Explore the concept further here


I am faced with a scenario where I have a function that takes one parameter, which can either be a custom type (QueryKey) or a function:

export function createHandler(
  createQueryKeyOrQueryKey: QueryKey | ((idForQueryKey: string) => QueryKey),
) { ... }

Depending on this parameter, the createHandler function needs to return different types:

  return {
    createState:
      typeof createQueryKeyOrQueryKey !== "function"
        ? (data) => createState(data, createQueryKeyOrQueryKey)
        : (data, id) => createState(data, createQueryKeyOrQueryKey(id)),
  };

The type of what is returned from createState is also subject to conditions:

createState:
  | ((data: TData) => State)
  | ((data: TData, idForQueryKey: string) => State);

When using createHandler to create a handler, there are two ways it can be utilized:

handler.createState({ a: 123 })
handler.createState({ a: 123 }, "some-id")

However, only one form of createState should be permissible at a time. The choice between the two should depend on how the handler is created, whether a query key OR a function is provided:

// Option 1:
// 
// Using query key directly
const queryKey = "some-query-key"
const handler1 = createHandler(queryKey)
// ✅ Allowed
handler1.createState({ a: 123 })
// ❌ Not allowed
handler1.createState({ a: 123 }, "some-id")

// Option 2:
//
// Using query key as a function for creation
const queryKeyCreator = (id: string) => "some-query-key" + id
const handler2 = createHandler(queryKeyCreator)
// ❌ Not allowed
handler2.createState({ a: 123 })
// ✅ Allowed
handler2.createState({ a: 123 }, "some-id")

At present, the return type does not work correctly, resulting in data being of type any:

Why is this happening? TypeScript recognizes that createState has conditional behavior and the variant with just one parameter (data) should also be a valid option based on the type of createState.


By the way, is there a better solution to tackle this issue? Perhaps utilizing function overloading or discriminating unions via keys could be feasible options, although implementing them based on the decision of the caller regarding which variant (key or function) to use poses some uncertainty.

Answer №1

What createState returns is also dependent on conditions

It's not straightforward. It involves a union, indicating that "createState can return either this type of function or that one, and it's up to you to determine which one by calling createState". The actual implementation details don't matter. In essence, if you explicitly provide the return type (meaning TypeScript doesn't infer it from the function's code), TypeScript views it as:

function createHandler<TData extends QueryData>(
  createQueryKeyOrQueryKey: ((idForQueryKey: string) => QueryKey) | QueryKey,
): {
  createState: 
   | ((data: TData, idForQueryKey: string) => State)
   | ((data: TData) => State);
} {
  // Implementation
}

TypeScript does not consider how createQueryKeyOrQueryKey and createState are related in the implementation. Conditional types in TypeScript look like ... extends ... ? ... : ....

Your scenario is suitable for overloads. Here's an example of how it could be done:

// First signature
// If a function is passed, return variant with `idForQuery`
function createHandler<TData extends QueryData>(
  createQueryKey: (idForQueryKey: string) => QueryKey
): {
  createState: (data: TData, idForQueryKey: string) => State
}
// Second signature
// If a string is passed, return simpler variant
function createHandler<TData extends QueryData>(
  queryKey: QueryKey
): {
  createState: (data: TData) => State
}
// Implementation signature
// Not visible to callers, only for typing inside function body
function createHandler<TData extends QueryData>(
  createQueryKeyOrQueryKey: ((idForQueryKey: string) => QueryKey) | QueryKey,
): {
  createState: 
   | ((data: TData, idForQueryKey: string) => State)
   | ((data: TData) => State);
} {
  return {
    createState:
      typeof createQueryKeyOrQueryKey !== "function"
        ? (data: TData) => createState(data, createQueryKeyOrQueryKey)
        : (data, id) =>
          createState(data, createQueryKeyOrQueryKey(id)),
  };
}

See sandbox

However, using actual conditional types may not be recommended because:

  1. I used declare function without providing the implementation deliberately, as it can be challenging to type correctly and may require type casts
  2. There are potential pitfalls such as losing type safety when using generics like any
  3. Handling two generics makes usage more complex due to TypeScript's binary approach to specifying all or none

While this method is informative, utilizing overloads is the preferred solution for simplicity and clarity.

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 are the best practices for transpiling code using Parcel-bundler?

Currently, I am attempting to transpile both .ts (TYPESCRIPT) and .scss (SASS) files. However, I am encountering two main issues: 1) Instead of generating my file in the designated dist directory, it is creating a dist directory within the build folder. ...

Error: Cannot locate 'import-resolver-typescript/lib' in jsconfig.json file

Issue: An error occurred stating that the file '/Users/nish7/Documents/Code/WebDev/HOS/frontend/node_modules/eslint-import-resolver-typescript/lib' could not be found. This error is present in the program because of the specified root file for c ...

Why is there a discrepancy between the value displayed in a console.log on keydown and the value assigned to an object?

As I type into a text box and log the keydown event in Chrome, I notice that it has various properties (specifically, I'm interested in accessing KeyboardEvent.code). Console Log Output { altKey: false bubbles: true cancelBubble: false cancelable: t ...

After filling a Set with asynchronous callbacks, attempting to iterate over it with a for-of loop does not accept using .entries() as an Array

Encountering issues with utilizing a Set populated asynchronously: const MaterialType_Requests_FromESI$ = SDE_REACTIONDATA.map(data => this.ESI.ReturnsType_AtId(data.materialTypeID)); let MaterialCollectionSet: Set<string> = new Set<s ...

The value of this.$refs.<refField> in Vue.js with TypeScript is not defined

During the process of converting my VueJs project to TypeScript, I encountered an error related to TypeScript. This issue arises from a component with a custom v-model implementation. In the HTML, there is an input field with a 'plate' ref that ...

The service remains operational while the button's status undergoes a change

In my data table, each row has a column containing buttons. To ensure that only the button in the clicked row is executed, I include the index of that row in the start/pause timer function. I decided to create these functions in a service so that the time ...

The functionality of ngModel is not functioning properly on a modal page within Ionic version 6

Currently I am working on an Ionic/Angular application and I have encountered a situation where I am attempting to utilize ngModel. Essentially, I am trying to implement the following functionality within my app: <ion-list> <ion-item> <ion ...

"Jest test.each is throwing errors due to improper data types

Currently, I am utilizing Jest#test.each to execute some unit tests. Below is the code snippet: const invalidTestCases = [ [null, TypeError], [undefined, TypeError], [false, TypeError], [true, TypeError], ]; describe('normalizeNames', ...

Provider not found: ConnectionBackend – NullInjectorError

I encountered the following error while attempting to load the webpage. Despite trying various suggestions from other sources, I have been unable to find a solution. Below the error stack lies my code. core.js:7187 ERROR Error: Uncaught (in promise): Null ...

Leveraging an external Typescript function within Angular's HTML markup

I have a TypeScript utility class called myUtils.ts in the following format: export class MyUtils { static doSomething(input: string) { // perform some action } } To utilize this method in my component's HTML, I have imported the class into m ...

Getting the Most Out of .find() in Angular 4

Despite reading various similar questions, I'm still struggling to make the .find() function work in my application. I have a service with the following API: export class VehicleService { private defUrl = 'API'; constructor(private ht ...

Having difficulty constructing a full stack application using Express

I've been struggling to configure a full stack app using create-react-app with Express and TypeScript. My main issue is figuring out how to compile the server files into a build folder. I have separate tsconfig files for the server and create-react-ap ...

What is the step-by-step process for incorporating the `module` module into a Vue project?

ERROR Compilation failed with 6 errors 16:20:36 This specific dependency could not be located: * module in ./node_modules/@eslint/ ...

What is the importance of using getters for functions involving Moment.js in vueJS and typescript?

weekOfMonth() calculates the current month and week within that month. <template> <h3>{{ weekOfMonth }}</h3> </template> <script lang="ts"> export default class HomeView extends Vue { const moment = require(& ...

Different Ways to Modify Data with the Change Event in Angular 8

How can I dynamically change data using the (change) event? I'm attempting to alter the gallery items based on a matching value. By default, I want to display all gallery items. public items = [{ value: 'All', name: 'All Item ...

Overloading TypeScript functions with Observable<T | T[]>

Looking for some guidance from the experts: Is there a way to simplify the function overload in the example below by removing as Observable<string[]> and using T and T[] instead? Here's a basic example to illustrate: import { Observable } from ...

Customizing Tabs in Material UI v5 - Give your Tabs a unique look

I am attempting to customize the MuiTabs style by targeting the flexContainer element (.MuiTabs-flexContainer). Could someone please clarify the significance of these ".css-heg063" prefixes in front of the selector? I never noticed them before upgrading my ...

Creating a singleton in TypeScriptWould you like to know how to declare a singleton in

My goal is to incorporate an already existing library into my TypeScript project. The library contains a singleton object that I want to declare and utilize. For example, within the xyz.js file, the following object is defined: var mxUtils = { /* som ...

Is there a way to verify if a user taps outside a component in react-native?

I have implemented a custom select feature, but I am facing an issue with closing it when clicking outside the select or options. The "button" is essentially a TouchableOpacity, and upon clicking on it, the list of options appears. Currently, I can only cl ...

Creating a dropdown menu in Bootstrap 5 without using any of the Bootstrap

In my Angular application, I have a header with icons and pictures that I would like to use as dropdown menus. The code snippet for this functionality is shown below: <li class="nav-item dropdown"> <a class="nav-li ...