Reputation: 784
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:(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
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!
Upvotes: 2