How to correctly type return values in child class (typescript)

Viewed 129

I want to create a base class for working with API (let's call it BaseAPI). Then, for each API, create a wrapper like UsersAPI, LoginAPI and such. So base class only encapsulates logic for making HTTP requests and aligning responses to same structure.

Minimal example:

type BaseResp = Record<string, unknown>

class BaseAPI {
    async get(url: string): Promise<BaseResp> {
        const resp = await fetch(url)
        return await resp.json()
    }
}

interface LoginResp extends BaseResp {
    username: string
}

class AuthAPI extends BaseAPI {
    async login(): Promise<LoginResp> {
        return await this.get("/login")
    }
}

The problem is that in fact BaseApi.get returns a Promise<any>, cause I don't know what data comes from the backend, so it's any.

But I know how server responds to /login API calls, so I want to receive typed object (LoginResp) after awaiting AuthAPI.login.

My linter is set up the way that it doesn't allow any usage, and I don't want to suppress it. That's why I tried to follow TS conventions and replace any with Record<string, unknown>.

Now I'm getting an error: Property 'username' is missing in type 'Promise<BaseResp>' but required in type 'LoginResp'.(2741)

How to set up typing correctly?

1 Answers

Since the result of json raw parsing will always be any (type of it cannot be determined at compile time) you have 2 choices.

1. You trust what comes back from the server.

In this case you can cast the parsed json data, you could modify your BaseApi type to something like this:

class BaseAPI {
    async get<T extends BaseResp>(url: string): Promise<T> {
        const resp = await fetch(url)
        return await resp.json() as T
    }
}

2. You don't trust what's coming back from the server

In this case you could set up type guards:

function isLoginResp(input: any): input is LoginResp {
  return typeof input === 'object' && typeof input.username === 'string'
}

And while keeping the rest of your code as is, you could change your AuthApi class:

class AuthAPI extends BaseAPI {
    async login(): Promise<LoginResp> {
        const response = await this.get("/login")
        if (isLoginResp(response)) {
          return response
        }
        throw new TypeError(`Expected LoginResp, got ${JSON.stringify(response)}`)
    }
}
Related