Search code examples
typescripttypesnarrowing

typescript type narrowing `data` is not undefined when `error` is undefined


I have the following interface for HTTP response

interface HTTPResponse<T> {
  ok: boolean, 
  error?: HTTPError; 
  data?: T, 
  init: ResponseInit
}

and this type narrowing check

export function isHTTPError(object: any): object is HTTPError {
  return "error" in object && object.ok === false;
}

when I pass a response through the isHTTPError check, typescript can tell if this is an error response and knows that error can't be undefined at this point.

If the response is not an error how to tell typescript that data is definitely present and should not be treated as possibly undefined?


At this moment I am using the following function

export function isHTTPSuccess<T>(object: any): object is T {
  return "data" in object && object.ok === true;
}

but this requires checking the response twice.


Solution

  • You want a discriminated union, where ok is the discriminant.

    You need two types. One for the success case, and another for the error case.

    interface HTTPResponseSuccess<T> {
      ok: true
      error?: undefined
      data: T
      init: ResponseInit
    }
    
    interface HTTPResponseError {
      ok: false
      error: HTTPError
      data?: undefined
      init: ResponseInit
    }
    

    These have the same properties, but different types. This will, for example, allow typescript to know that if ok is true then there is no error and data exists.

    Now you can build a union of these types to use when you don't know the result.

    type HTTPResponse<T> = HTTPResponseSuccess<T> | HTTPResponseError
    

    And now you can simply do:

    // mock a response where you dont yet know if it was a success.
    const res: HTTPResponse<number> = await someApi.getSomething()
    
    if (res.ok) {
      console.log(res.data.toFixed())
    }
    

    See playground