Altaula
Altaula

Reputation: 784

TypeScript Generic function return type issue

I want to define the function return type via TypeScript Generics. So the R can be anything what I will define.

... Promise<R | string> is not solution for me.

Error

Error:(29, 9) TS2322: Type 'string' is not assignable to type 'R'. 'string' is assignable to the constraint of type 'R', but 'R' could be instantiated 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;

Upvotes: 1

Views: 2650

Answers (1)

jcalz
jcalz

Reputation: 327819

I'd probably use overloads to represent this distinction from the caller's side... if the caller specifies "text" then the return type is definitely Promise<string> and the function is not generic in R anymore.

Aside: TypeScript naming conventions usually reserve single-character uppercase names for generic type parameters (especially T, U, K, and P), so I will expand your P to Params. Also, the identifier as is problematic because it is a reserved word in TypeScript and might confuse the IDE or compiler; I will replace as with az in what follows. Okay, so your interface is

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 overloads I'd use. One non-generic call signature which only accepts an az of "text", and the other is generic in R and only accepts an az of "json" or undefined/missing. The implementation signature can involve R | string or any or whatever you want, since it is invisible from the caller's side.

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("!");
}

And here's 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 (which requires that the caller specify R because it can't be inferred):

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

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

And note that you can't ask for "text" if you've specified R:

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

Okay, I hope this helps you. Good luck!

Link to code

Upvotes: 2

Related Questions