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!