1
votes

TYPESCRIPT playground

Exists something like Overloads for class? I created previous issue associated with createRequest.ts and the function should be without error. I want to define same generics for class Data like generics in createRequest function.

Error

Error:(31, 52) TS2345: Argument of type 'RequestParameters' is not assignable to parameter of type 'RequestParameters & { as?: "json" | undefined; }'. Type 'RequestParameters' is not assignable to type '{ as?: "json" | undefined; }'. Types of property 'as' are incompatible. Type '"json" | "text" | undefined' is not assignable to type '"json" | undefined'. Type '"text"' is not assignable to type '"json" | undefined'.

Data.tsx

import * as React from 'react';

import createRequest, { RequestParameters, } from '../../createRequest';

interface P<R> {
  children: (children: { createRequest: Data<R>['createRequest'] } & S<R>) => React.ReactNode;
  parameters: RequestParameters;
  url: Parameters<typeof createRequest>[0];
}

interface S<R> {
  error: Error | null;
  response: R | null;
}

class Data<R> extends React.Component<P<R>, S<R>> {
  async componentDidMount () {
    await this.createRequest();
  }

  state = { error: null, response: null, };

  createRequest = async (): Promise<void> => {
    this.setState(() => ({ error: null, response: null, }));

    try {
      const { parameters, url, } = this.props;

      const response = await createRequest<R>(url, parameters); // <-- Error

      this.onResponse(response);
    } catch (error) {
      this.onError(error);
    }
  };

  onError (error: Error): void {
    this.setState(() => ({ error, }));

    throw error;
  }

  onResponse (response: R): void {
    this.setState(() => ({ response, }));
  }

  render () {
    const { children, } = this.props;

    return children({ createRequest: this.createRequest, ...this.state, });
  }
}

export default Data;

createRequest.ts

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

export interface RequestParameters {
  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 (url: URL | string, parameters: RequestParameters & { as: 'text' }): Promise<string>;
async function createRequest<R> (url: URL | string, parameters: RequestParameters & { as?: 'json' }): Promise<R>;
async function createRequest<R> (url: URL | string, parameters: RequestParameters): Promise<R | string> {
  if (isString(url)) {
    url = new URL(url);
  }

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

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

  if (response.ok) {
    switch (parameters.as) {
      case 'json':
        return response.json();
      case 'text':
        return response.text();
      default:
        return response.json();
    }
  }

  throw new Error('!');
}

export default createRequest;
1

1 Answers

0
votes

The function createRequest() is overloaded but you are trying to use a single call for possibly both overloads, which doesn't work. Overloads are almost like different functions from the caller's side... imagine you had createRequestJson() and createRequestText() and think about how you'd call those at once... it would need to look something like someTest(parameters) ? createRequestJson<R>(url, parameters) : createRequestText(url, parameters) where someTest(parameters) was a type guard function whose result would narrow parameters to the appropriate type.

The absolute simplest way to address this is to steamroll the compiler by asserting that parameters is of an acceptable type for the generic overload, such as the wildcard any type:

const response = await createRequest<R>(url, parameters as any); // no error

Any other fix is likely to require quite a lot of refactoring, because your Data<R> and P<R> types are swallowing the distinction between json requests and text requests. So createRequest() cares about this distinction (which is why you got your error) but nothing else in the calling code does. Data<R> is perfectly happy to represent a non-string R with properties including a RequestParameters whose as property is "text". If you're okay with that you might as well just push the assertion down into createRequest() to get this non-overloaded function:

async function createRequest<R>(
  url: URL | string,
  parameters: RequestParameters
): Promise<R> {
  if (typeof url === "string") {
    url = new URL(url);
  }

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

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

  if (response.ok) {
    switch (parameters.as) {
      case "json":
        return response.json();
      case "text":
        return (response.text() as any) as R; // assertion
      default:
        return response.json();
    }
  }

  throw new Error("!");
}

and just deal with the fact that someone can call createRequest<SomeRandomThing>(url, {as: "text"}), maybe with a warning in the documentation. Frankly I'd probably go this route, since the alternative is to split all your types and classes up into json and text flavors. It's not impossible, and if you care a lot about type safety you could do it. But I doubt you care that much, since createRequest<R>(url, {as: "json"}) is already not type-safe when the caller can pass in any random type for R.

Okay, hope that helps; good luck!

Link to code