sunknudsen
sunknudsen

Reputation: 7270

How to set method return type based on constructor options?

Developing an open source abstraction on top of got that implements rate limiting.

Everything works great except I’m unable to figure out how to set the return type of post, get, patch, put and delete methods to the responseType provided using constructor options (see responseType: options?.responseType ?? "json",).

I used overrides to set return type using the responseType method argument (when provided), but I don’t know how to do that for constructor options. Could really use a hand on this!

Btw, aside from an answer here, feel free to submit a PR to https://github.com/sunknudsen/http if you want to contribute to the project.

type ResponseType = "buffer" | "json" | "text"
interface HTTPOptions { responseType?: ResponseType }
interface HTTPResponse<T = unknown> { body: T }

class HTTP {
  constructor(public options: HTTPOptions) {    
    this.options.responseType = options.responseType ?? "json"; // Default response type is JSON
  }  
  public get( url: string, options: HTTPOptions & { responseType: "buffer" }): Promise<HTTPResponse<Buffer>>
  public get(url: string, options: HTTPOptions & { responseType: "json" }): Promise<HTTPResponse<JSON>>
  public get(url: string, options: HTTPOptions & { responseType: "text" }): Promise<HTTPResponse<string>>
  public get(url: string, options?: HTTPOptions): Promise<HTTPResponse>
  public get(url: string, options?: HTTPOptions): Promise<HTTPResponse> {
    return null!
  }  
}


let http = new HTTP({ responseType: "text" });

http.get("", { responseType: "text" }) // string 
http.get("") // expected string, this does not work

Playground Link

Upvotes: 1

Views: 117

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249676

The first part of the problem is capturing the type information in the constructor. We need to know the type of responseType passed when the class is created. Whenever we need to capture call site info, a generic type parameter is usually the way to go.

So we can change the class to :

class HTTP<TResponseDefault extends ResponseType = "json"> {
  constructor(public options: HTTPOptions & { responseType?: TResponseDefault }) {    
    this.options.responseType = options.responseType ?? "json" as TResponseDefault; // Default response type is JSON
  }
}  

This will ensure we have the necessary info in the method, as we have captured in the type of any created HTTP object when responseType is passed in:

let httpJson = new HTTP({}); //  HTTP<"json">
let httpText = new HTTP({ responseType: "text"}); //  HTTP<"text">

Now that we have this info, we can see about making the return type dependent on it. While overloads are usually a good first approach to this, in this case since the default can come from either the the class or the call, and because there are many options and methods, I would go with another approach.

What we can do is make the method generic with a generic type parameter to capture the response type. This generic type parameter will default to the class type parameter. So if no response type is specified in the call TResponseDefault will be used. We can then use this type parameter to index into an interface where we map from response types to actual types:


interface TypeToResponseType {
  "buffer" : HTTPResponse<Buffer>
  "json" : HTTPResponse<object>
  "text" : HTTPResponse<string>
}

class HTTP<TResponseDefault extends ResponseType = "json"> {
  constructor(public options: HTTPOptions & { responseType?: TResponseDefault }) {    
    this.options.responseType = options.responseType ?? "json" as TResponseDefault; // Default response type is JSON
  }  

  public get<TResponse extends ResponseType = TResponseDefault>(url: string, options?: HTTPOptions & { responseType?: TResponse }): Promise<TypeToResponseType[TResponse]>
  public get(url: string, options?: HTTPOptions): Promise<HTTPResponse> {
    return null!
  }  
}


let httpJson = new HTTP({}); //  HTTP<"json">
let httpText = new HTTP({ responseType: "text"}); //  HTTP<"text">

let a = httpJson.get("", { responseType: "text" }) // string 
let b = httpJson.get("") // object
let c = httpText.get("") // string 

Playground Link

Upvotes: 1

Related Questions