Multiple conditions in TypeScript resulting in a function output

I am working on developing a function that can return different types based on its parameters.

Similar to the function below, but I want it to be more streamlined and efficient (using generics without union types as results)

type ResultType = {
  get: GetResult
  post: PostResult
  ...
}

function fetch(operation: keyof ResultType): GetResult | PostResult | ...

To start, I referenced an example from https://www.typescriptlang.org/docs/handbook/2/conditional-types.html. However, when trying to implement the function from the example:

interface IdLabel {
  id: number /* + other fields */
}
interface NameLabel {
  name: string /* + other fields */
}

type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  if (typeof idOrName === 'number') {
    return { id: idOrName }
  } else {
    return { name: idOrName }
  }
}

I encounter errors that are confusing to me:

Type '{ id: number; }' is not assignable to type 'NameOrId<T>'.
Type '{ name: string; }' is not assignable to type 'NameOrId<T>'.

What mistake did I make?

Additionally, is there another way in TypeScript to handle multiple conditional cases effectively (similar to a switch statement)?

Answer №1

I have a solution for the initial problem you presented, which involves linking the return type to the input type.

The first example provides a basic but non-type-safe solution. The second example is more complex but ensures complete type safety (to the best of my knowledge).

To define the return type based on the input type, you can utilize an indexed access type.

Unfortunately, TypeScript does not fully support this functionality yet. There is an open issue regarding proper support labeled as "Dependent-Type-Like Functions" at ms/TS#33014. A workaround for simpler cases involves asserting the type of the returned values.

The code snippet below illustrates a potential approach, including my own definitions for missing types.

type GetResult = {
  getData: string
}

type PostResult = {
  postData: string
}

type ResultType = {
  get: GetResult
  post: PostResult
}

function fn<T extends keyof ResultType>(operation: T): ResultType[T] {
  if (operation === "get") {
    return { getData: "foo" } as ResultType[T]
  } else {
    return { postData: "bar" } as ResultType[T]
  }
}

const res1 = fn("get")
//     ^? GetResult
const res2 = fn("post")
//     ^? PostResult
const res3 = fn("put") // Error: "put" is not a key of ResultType

Explore the above code on TS Playground

You could implement a function map and define ResultType in relation to it. By establishing a connection between the function map and the type, full type-safety can be achieved! (Credit to @jcalz for highlighting this aspect.)

Below is a possible implementation for this concept:

type GetResult = {
  getData: string
}
type PostResult = {
  postData: string
}

const _operations = {
  get(): GetResult {
    return { getData: "foo" }
  },
  post(): PostResult {
    return { postData: "bar" }
  },
}

type ResultType = {
  [key in keyof typeof _operations]: ReturnType<(typeof _operations)[key]>
}

const operations: { [K in keyof ResultType]: () => ResultType[K] } = _operations

function fn<T extends keyof ResultType>(operation: T): ResultType[T] {
  return operations[operation]()
}

Check out the code on TS Playground

_operations maps operation names to functions. From this mapping, we construct ResultType. However, there is no direct link between _operations and ResultType; instead, ResultType is defined based on it. Therefore, operations is assigned the same value as _operations, but with a type annotation connecting it to ResultType. This linkage helps TypeScript understand the relationship between the operation name and its corresponding return type.

Answer №2

It's true, discriminated unions may not be the most helpful in this case.

I believe I've come up with a solution, but I'm unsure if it aligns with your preferences. Feel free to review and provide feedback.

This solution revolves around utilizing Indexed Access Types.

We can envision a set of types, potentially intricate as per your suggestion. Personally, I lean towards using type over interface, although both should be effective.

type TIdLabel = {
  id: number /* plus additional fields */
}

type TNameLabel = {
  name: string /* plus additional fields */
}

type TMixedLabel = {
  num: number
  text: string;
  flag: boolean;
}

Next, we define our function along with its input and output types. Each type is assigned to a specific key for indexing purposes.

type TResult = {
  "id": TIdLabel,
  "name": TNameLabel,
  "mixed": TMixedLabel,
}

type TProps = {
  "id": number,
  "name": string,
  "mixed": {
    a: number;
    b: number;
  },
}

function createNewLabel<T extends keyof TResult>(type: T, props: TProps[T]): TResult[T] {
  // Implementation details here
}

The discriminator part seems a bit cumbersome, needing runtime checks. However, the type argument serves as a distinguishing factor.

Usage is straightforward, albeit requiring you to specify the type for discrimination.

const label1 = createNewLabel("id", 123);        
const label2 = createNewLabel("name", "Alice");  
const label3 = createNewLabel("mixed", { a: 7, b: 10 });  
//const label4 = createNewLabel("fail");  //404 error

It appears possible to eliminate the explicit discriminator, assuming clear differentiation against the input.

function createNewLabel2<T extends keyof TResult>(props: TProps[T]): TResult[T] {
  // Revised implementation excluding explicit discriminator
}

const newLabel1 = createNewLabel2(456);
const newLable2 = createNewLabel2("Eve");
const newLable3 = createNewLabel2({ a: 8, b: 12 });

Experiment with the code in this interactive playground.

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

Use JavaScript to swap out various HTML content in order to translate the page

I am currently facing a challenge with my multilingual WordPress website that utilizes ACF-Field. Unfortunately, WPML is not able to translate the field at the moment (2nd-level-support is looking into it). As a solution, I have been considering using Java ...

The requested external js scripts could not be found and resulted in a net::ERR_ABORTED 404 error

import express, { Express, Request, Response } from 'express'; const path = require("path"); import dotenv from 'dotenv'; dotenv.config(); const PORT = process.env.PORT || 5000; const app = express(); app.use(express.static(path.join ...

Issue with command execution within execSync in node.js

I am facing an issue where a shell command works fine from the terminal, but when I try to run it from node.js, it gives me an error. Original Command awk -v RS='"[^"]*"' '{n+=gsub(/\n/, "&")} END{print n}& ...

Searching for and modifying a specific subdocument in Mongoose

I am currently working with the schema for a document called Folder: var permissionSchema = new Schema({ role: { type: String }, create_folders: { type: Boolean }, create_contents: { type: Boolean } }); var folderSchema = new Schema({ nam ...

Guide on serving static HTML files using vanilla JavaScript and incorporating submodules

Is it possible to serve a static html file with elements defined in a javascript file using imports from submodules using vanilla JS? Or do I need bundling tools and/or other frameworks for this task? Here's an example code structure to showcase what ...

Experiencing Problems with Bot Framework Authentication - Receiving HTTP 401 Error

My current project involves creating a chat bot using the Microsoft Bot Framework and SDK in TypeScript. I am working on implementing user authentication for the bot to interact with Azure DevOps on behalf of users. While testing authentication in Azure Po ...

Use VueJS v-on:click and Vanilla JS to collapse various divs when clicked

Can VueJS and vanilla JS be used to collapse specific divs among many? I have information contained in separate card elements that include a title and a body. My goal is to make the body of each card expand or collapse when the respective title is clicked ...

Keeping an eye on the location and retrieving articles once more - Vuejs

ClassController <?php namespace App\Http\Controllers\Classes; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Models\ClassCategory; use App\Models\Article; use App& ...

What steps should I take to design and implement this basic search form?

Essentially, I have a three-page setup: - One page containing all possible search results such as: 'search result 1' 'search result 2' 'search result 3' - Another page with a search form and enter button. - And finally, a res ...

Is there a way to override the log file in Node.js when it reaches the file size limit without enabling the createSy

Currently, I have a NodeJS service operating within a Docker container and attaching the log file to another service (also within Docker) that processes the file and sends it to ElasticSearch (referred to as the exporter). Unfortunately, the exporter is un ...

Guide on dynamically updating a div in PHP with a mix of text output and images at regular intervals

My current project involves creating a browser-based card game primarily using PHP, with potentially incorporating other languages to help me enhance and test my PHP skills. However, I've encountered difficulties while attempting to implement various ...

Is there a way to activate decorator support while running tests using CRA 2.1?

Struggling to set up testing for a React application utilizing decorators and Typescript within Create React App v2.1.0 Aware that official support for decorators is lacking. Successfully running the application with the help of React App Rewired and @ba ...

Adjusting the transparency of each segment within a THREE.LineSegments object

I am following up on a question about passing a color array for segments to THREE.LineSegments, but I am looking for a solution that does not involve low-level shaders. I am not familiar with shaders at all, so I would prefer to avoid them if possible. I ...

Ensuring that $.each properly processes all deferreds in jQuery

I am currently working with the following jQuery code: myFunc: function(cmd, obj){ var idToExtMap = this.map; var requireRes = this.reqInst; var deferreds = []; var ret = true; $.each(idToExtMap[cmd], function( ...

Converting an array of arrays to an array of objects in a React application

I have an array of arrays as follows: const arrayOfArrays = [ ['Lettuce', 60], ['Apple', 80] ]; What is the best way to transform it into an array of objects with keys for name and price, like this: const arrayOfObjects = [ {name: ...

Displaying data on the user interface in Angular by populating it with information from the form inputs

I am working on a project where I need to display data on the screen based on user input received via radio buttons, and apply specific conditions. Additionally, I require assistance in retrieving the id of an object when the name attribute is chosen from ...

The map buttons are located underneath the map, and unfortunately, it seems that setting the map height to 100% using Angular is

Upon completing the creation and display of the map, an unusual occurrence is taking place where the map buttons ("Zoom rectangular, map settings, and scale bar") are appearing below the map as oversized icons. Additionally, there is a challenge when setti ...

Python web scraping woe: BeautifulSoup fails to display complete HTML source code

I'm a newcomer to the world of web scraping and Python. I've been working on a script to extract the Last Trade Price from this website, but it seems that some content is not being retrieved properly when using Python. While I've successfull ...

Retrieving attribute of a span element inside a nested div

Newbie to web scraping and facing an issue. I am trying to extract the value of data-value from the span with class "DFlfde SwHCTb". However, I keep getting an undefined return. Can someone point out what error I made in the code below? const axios = requi ...

Arranging a collection of objects in alphabetical order

My current challenge involves sorting an array of objects alphabetically, and to simplify things, I have provided the example below. In my TypeScript code, I utilize splice to add and remove items from the object array. Array cars = [{ id: 1, ...