The challenge of return types in TypeScript generic functions

My goal is to set the return type of a function using TypeScript Generics. This way, the R can be defined as anything I choose.

... Promise<R | string> doesn't work for me.

Error

Error:(29, 9) TS2322: Type 'string' is not assignable to type 'R'. 'string' can be assigned to the constraint of type 'R', but 'R' might end up with a different subtype of constraint '{}'.

import { isString, } from '@redred/helpers';

interface P {
  as?: 'json' | 'text';
  body?: FormData | URLSearchParams | null | string;
  headers?: Array<Array<string>> | Headers | { [name: string]: string };
  method?: string;
  queries?: { [name: string]: string };
}

async function createRequest<R> (url: URL | string, { as, queries, ...parameters }: P): Promise<R> {
  if (isString(url)) {
    url = new URL(url);
  }

  if (queries) {
    for (const name in queries) {
      url.searchParams.set(name, queries[name]);
    }
  }

  const response = await fetch(url.toString(), parameters);

  if (response.ok) {
    switch (as) {
      case 'json':
        return response.json();
      case 'text':
        return response.text(); // <- Error
      default:
        return response.json();
    }
  }

  throw new Error('!');
}

export default createRequest;

Answer №1

To distinguish from the caller's side, I would opt for using overloads... if the caller specifies "text", then the return type will be Promise<string>, and the function won't be generic in R.

Note: TypeScript conventions typically assign single-character uppercase names for generic type parameters (such as T, U, K, and P). Therefore, I'll expand your P to Params. Additionally, since as is a reserved word in TypeScript, I'll use az instead. So, your interface will be:

interface Params {
  az?: "json" | "text";
  body?: FormData | URLSearchParams | null | string;
  headers?: Array<Array<string>> | Headers | { [name: string]: string };
  method?: string;
  queries?: { [name: string]: string };
}

Here are the proposed overloads. One non-generic signature that accepts only az of "text", and another generic one in R that allows az values as "json" or undefined/missing. The implementation can involve R | string or any since it remains hidden from the caller.

async function createRequest(
  url: URL | string,
  { az, queries, ...parameters }: Params & { az: "text" }
): Promise<string>;
async function createRequest<R>(
  url: URL | string,
  { az, queries, ...parameters }: Params & { az?: "json" }
): Promise<R>;
async function createRequest<R>(
  url: URL | string,
  { az, queries, ...parameters }: Params
): Promise<R | string> {
  if (isString(url)) {
    url = new URL(url);
  }

  if (queries) {
    for (const name in queries) {
      url.searchParams.set(name, queries[name]);
    }
  }

  const response = await fetch(url.toString(), parameters);

  if (response.ok) {
    switch (az) {
      case "json":
        return response.json();
      case "text":
        return response.text(); // <- okay now
      default:
        return response.json();
    }
  }

  throw new Error("!");
}

Below is how we'd use it to get text:

const promiseString = createRequest("str", { az: "text" }); // Promise<string>

And here's how we'd use it to get some other type by specifying R:

interface Dog {
  name: string;
  age: number;
  breed: string;
  fleas: boolean;
}

const promiseDog = createRequest<Dog>("dog", {}); // Promise<Dog>

Do remember that you can't request "text" when specifying R:

const notGeneric = createRequest<Dog>("dog", {az: "text"}); // error!
//                                     -----> ~~
// "text" is not assignable to "json" or undefined

I hope this explanation is valuable to you. Good luck!

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

Error: Typings: Invalid syntax - Unexpected symbol =>

Every time I run a typings command, I encounter the following error: AppData\Roaming\npm\node_modules\typings\node_modules\strip-bom\index.js:2 module.exports = x => { ^^ SyntaxError: Unexpected tok ...

Retrieving the value of an object using an array of keys

Consider the following object: const obj = { A:{ a1:'vala1', a2:'vala2' }, B:{ b1: 'valb1', b2: 'valb2' }, C:{ c1:{ c11:'valc11' }, c2:'valc2' } } We also have an array: const ...

What methods can be used to eliminate superfluous `require(...)` or `import "...` statements while working with Rollup?

Currently, I am in the process of developing a library named share which will be utilized in various other services. To bundle this library, I have opted for Rollup.js. share serves as a common library accessed by multiple services, therefore it is essent ...

Cropping and resizing images

Within my angular application, I have successfully implemented image upload and preview functionality using the following code: HTML: <input type='file' (change)="readUrl($event)"> <img [src]="url"> TS: readUrl(event:any) { if ...

Is there a way to mock a "find" call in mockingoose without getting back "undefined"?

I am currently working with mockingoose 2.13.2 and mongoose 5.12.2, leveraging Typescript and jest for testing purposes. Within my test scenario, I am attempting to mock a call to my schema's find method. Here is what I have tried: import mockingoose ...

How to set up scroll restoration for the Angular Standalone Router?

The Angular Router provides the option to restore scrolling functionality, with documentation on how to implement it when loading data. Is there a way to configure the standalone router to automatically scroll back to the top of the router outlet? A demo ...

Angular: controller's property has not been initialized

My small controller is responsible for binding a model to a UI and managing the UI popup using semantic principles (instructs semantic on when to display/hide the popup). export class MyController implements IController { popup: any | undefined onShow(con ...

What is the best way to handle missing values in a web application using React and TypeScript?

When setting a value in a login form on the web and no value is present yet, should I use null, undefined, or ""? What is the best approach? In Swift, it's much simpler as there is only the option of nil for a missing value. How do I handle ...

The stream.write function cannot be executed as a callable expression

Struggling to create a function that accepts either a writable stream (createWriteStream) or process.stdout/.stderr in TypeScript, but encountering an error. import { createWriteStream, WriteStream } from 'fs' const writehello = (stream: NodeJS. ...

Looking for the type definition file for the xss-clean npm library?

Recently, I embarked on a journey to learn Typescript and began the process of converting my Node.js/Express application to use it. Thus far, I have managed to acquire all the necessary type definitions for libraries by running npm i @types/some-lib. The ...

Error message: When working with Protobuf, an uncaught reference error occurs because the 'exports

Currently, I am in the process of configuring protobuf to work with typescript. According to the official Google Documentation, all that is needed is to execute npm install google-protobuf and then to include require('google-protobuf'). Unfortu ...

Utilizing a GLTF asset as the main Scene element in a Three.js project

I'm struggling with incorporating a gltf model as the main scene in Three.js. Specifically, I have a gltf model of an apartment that I want to load from inside and not outside the apartment. I also need the controls to work seamlessly within the apart ...

If I exclusively utilize TypeScript with Node, is it possible to transpile it to ES6?

I am developing a new Node-based App where browser-compatibility is not a concern as it will only run on a Node-server. The code-base I am working with is in TypeScript. Within my tsconfig.json, I have set the following options for the compiler: { "inc ...

Leverage the template pattern in React and react-hook-form to access a parent form property efficiently

In an effort to increase reusability, I developed a base generic form component that could be utilized in other child form components. The setup involves two main files: BaseForm.tsx import { useForm, FormProvider } from "react-hook-form" expor ...

What is the reason behind the inability to invoke a method when a class explicitly inherits from Object?

Issues arise when attempting to call an object method of a TypeScript class that explicitly extends Object: class Example extends Object { constructor() { super(); } public getHello() : string { return "Hello"; } } let gre ...

Using type declaration, how can one create an arrow function?

I've been working on converting the following JavaScript code to TypeScript: users.map( item => ( { name: item.name, email: item.email, ...item.user } )); The structure of users looks like this: users = [ { name: &quo ...

Display and conceal table columns dynamically in Vue by utilizing the Vuetify data table functionality

Looking for an example: How to show hide columns of vuetify data table using v-select list I have created something similar, but I'm facing an issue where the table doesn't refresh when changing the header data: https://codepen.io/Meff1/pen/vY ...

Equivalents of If Statements in TypeScript using the Unique Walrus Operator

Similar to a previous question regarding the Walrus operator, but focused on if statements: import { argv } from "process" function foo(input: string): boolean { return input === "ppp"; } for (let i=0, v; v = foo(process.argv[2]) ...

The data returned from the useFetch function is currently unavailable

const { response, setResponse } = useResponseState(); const handleNext = () => { if ( response.currentResponse !== undefined && response.responses!== undefined ) { if (response.currentResponse < response.responses.l ...

Encountering the error message "React child is not valid as a Gatsby wrapRootElement" while using TypeScript with Gatsby

I've been exploring ways to implement a theme provider in Gatsby using the wrapRootElement browser API. However, I seem to have hit a roadblock as I keep encountering an error message that says "Objects are not valid as a React child (found: object wi ...