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

Is it feasible to define a custom Type in Typescript that accurately represents a Treeview structure?

Typescript is a TYPED superset of JavaScript that compiles into JavaScript, fine! It helps us to reduce some typos etc. I want to create an interface that will serve as an argument in a method. This interface needs to represent a treeview for parsing an ob ...

Angular authentication guard does not redirect properly after returning a Promise<UrlTree>

My authentication guard is set up to control access to the /sign-in and /verify-email routes, allowing only users who are not logged in or have not verified their email: export const signInGuard: CanActivateFn = (activatedRouteSnapshot: ActivatedRouteSnap ...

Route protection is ineffective when dealing with two observables simultaneously

After writing the route guard as shown below, I encountered an issue with the else statement that was not returning a result, even though it should have. Surprisingly, there were no errors either. this.hotelSettingsService.get().pipe(map(res => { ...

What is the best approach for managing _app.js props when transitioning from a page router to an app router?

Recently, in the latest version of next.js 13.4, the app router has been upgraded to stable. This update prompted me to transition my existing page router to utilize the app router. In _app.jsx file, it is expected to receive Component and pageProps as pr ...

Transform TypeScript class into an object

Is there a way to transfer all values from one typescript class, Class A, to another matching class, Class B? Could there be a method to extract all properties of Class A as an object? ...

Is it possible to make the 'keyof' property optional?

Illustrate an interface in the following way interface Properties { apple?: string banana?: string cherry?: string date: string } Executing this code works as expected type Sample1 = { [P in keyof Properties]: Properties[P] } const s1: Sample1 ...

When using React and Material UI, there seems to be an issue with the Popover component where calling `setAnchorEl(null)` on the onClose event does not properly

I am encountering an issue with a Popover (imported from MaterialUI) nested inside a MenuItem (also imported from MaterialUI). The open prop for the popover is set to the boolean value of anchorEl. The onClose function is supposed to handle setting anchorE ...

I'm struggling to figure out how to specify the data type for storing an array of objects in a variable using React.useState

I am currently working on extracting values from an .xlsx file using the SheetJS library. Below, I will provide the code snippets, errors encountered, and the different approaches I have attempted. Data extracted from var dataToJson: (6) [{…}, {…}, { ...

A guide on simulating childprocess.exec in TypeScript

export function executeCommandPromise(file: string, command: string) { return new Promise((resolve, reject) => { exec(command, { cwd: `${file}` }, (error: ExecException | null, stdout: string, stderr: string) => { if (error) { con ...

Export an array of objects using the Angular XLSX library

Here is my example data: exampleData: any[] = [ { "id": "123", "requestType": "Demo", "requestDate": "12/05/21", "status": "Success", "product": [ { "productName": "example product A", "productQty": "8" ...

Using Vue in conjunction with TypeScript and CSS modules

I am facing an issue with my SFC (single file vue component) that utilizes TypeScript, render functions, and CSS modules. <script lang="ts"> import Vue from 'vue'; export default Vue.extend({ props: { mode: { type: String, ...

How can I personalize a Leaflet popup with image thumbnails and additional customization options?

I've been researching and trying out different solutions, but so far none of them have worked for me. My goal is to dynamically add a title, image, address, and name to popups on my leaflet map as data comes in. However, I'm experiencing some cha ...

Transform JSON into an Array and generate a new Array from a collection of Arrays

I'm struggling with generating a table in Angular2 from JSON data because I need to pivot the information, but my usual method doesn't seem to work for this scenario. Below is an excerpt of the JSON data I am working with: [ { "ValueDate" ...

Having trouble with my ag-grid context menu not functioning properly

I am aiming to display a context menu when the user right-clicks on a cell. Unfortunately, my code is not responding to the right click event. I have been unable to locate where I may have made a mistake. Here is the snippet of my code that seems to be c ...

Utilizing Angular to Handle Undefined Variables in String Interpolation

Seeking a way to showcase data obtained from an external API on a webpage using Angular's string interpolation. If no data is retrieved or is still pending, the aim is to display 'N/A'. An attempt was made following this method, but encoun ...

The setupFile defined in Jest's setupFilesAfterEnv configuration is not recognized by the VSCode IDE unless the editor is actively open

I've put together a simplified repository where you can find the issue replicated. Feel free to give it a try: https://github.com/Danielvandervelden/jest-error-minimal-repro Disclaimer: I have only tested this in VSCode. I'm encountering diffic ...

Angular 10 Reactive Form - Controlling character limit in user input field

I'm currently developing an Angular 10 reactive form and I am looking for a way to restrict the maximum number of characters that a user can input into a specific field. Using the maxLength Validator doesn't prevent users from entering more chara ...

The scroll animation feature was not functioning properly in Next.js, however, it was working flawlessly in create react app

I recently transitioned a small project from Create React App (CRA) to Next.js. Everything is working as expected except for the scroll animations in Next.js, which are not functioning properly. There are no errors thrown; the animations simply do not occ ...

Easy steps to bring in type definitions from an npm package using Vite

I am currently developing a package with several ts functions that will be utilized by multiple repositories, including mobile and web applications. In our team, we use vite as our primary build tool, which is also integrated into the repository. Below is ...

Issue encountered: Upon executing 'ng add @angular/fire', an error was detected in the node_modules/firebase/compat/index.d.ts file while attempting to launch the Angular application

Recently, I decided to integrate Firebase into my project, but encountered a persistent error after installing the firebase module 'ng add @angular/fire' and running it with 'ng serve': Error: node_modules/firebase/compat/index.d.ts:770 ...