Home > front end >  Typescript Infer return value for multiple Error subclasses
Typescript Infer return value for multiple Error subclasses

Time:11-05

I have a function that can return a number, an Error, or a NegativeError:

class NegativeError extends Error {
  name = 'NegativeError'
}

function doThing (a: number): number | NegativeError | Error {
  return a < 0 ? new NegativeError() : a === 0 ? new Error() : a
}
const done = doThing() // T | NegativeError | Error

However, when I remove the signature's explicit return type, typescript drops the Error & only infers T | NegativeError as return type:

function doThing (a: number) {
  return a < 0 ? new NegativeError() : a === 0 ? new Error() : a
}
const done = doThing() // T | NegativeError

As a hack, I can force it to keep the return types discrete by using a constructor function, which maintains the prototypical inheritance:

function NegativeError(message: string) {
  Error.call(this)
  this.message = message
  this.name = 'NegativeError'
}
NegativeError.prototype = Object.create(Error.prototype)
NegativeError.prototype.constructor = NegativeError

I'd like to avoid this because it isn't easy to read & contains the typescript error: An outer value of 'this' is shadowed by this container.

Is there any way to make typescript correctly infer all return types?

CodePudding user response:

This looks like you're falling afoul of Typescript's structural type system. Your NegativeError class is structurally equivalent to the Error class, so as far as Typescript is concerned, these two types are subtypes of each other:

// type Test = true
type Test = Error extends NegativeError ? true : false

So since Error is a subtype of NegativeError, the union NegativeError | Error can be simplified to just NegativeError, or alternatively, since NegativeError is also a subtype of Error, the union Error | NegativeError can be simplified to just Error; either way.

This wouldn't happen in a language with a nominal type system (e.g. Java), but in Typescript the fact that NegativeError is declared as a subclass and has a different name, does not mean it is a different type. If you want to distinguish it then you can change its structure, e.g. by refining the type of the name property:

class NegativeError extends Error {
  name: 'NegativeError' = 'NegativeError'
}

Now Error is no longer a subtype of NegativeError, but NegativeError is still a subtype of Error, so the union NegativeError | Error | number will be simplified to Error | number. Which should be fine, because NegativeError is still assignable to that union type.

CodePudding user response:

It's not a direct solution to the core inference issue but yet another workaround is to wrap your "normal" error into another subtype :

class NegativeError extends Error {
  name = 'NegativeError'
}
class BError extends Error {
  name = 'BError'
}

function doThing(a: number) {
  return a < 0 ? new NegativeError() : a === 0 ? new BError() : a
}
const done = doThing(1) // infers doThing(a: number): number | NegativeError | BError

CodePudding user response:

Since typescript looks for extend to determine the super, I determined I couldn't use that.

I tried to extend an Error without using the extend keyword. I achieved that by setting the NegativeError prototype equal to the Error prototype:

class NegativeError {
  message: string
  name: 'NegativeError' = 'NegativeError'

  constructor(message: string) {
    this.message = message
  }
}
Object.setPrototypeOf(NegativeError.prototype, Error.prototype)

Thanks to @kaya3 for pointing me in the right direction!

  • Related