I am trying to 'replicate' Rust's Result
library to TS.
I have a BaseResult
implementation:
interface BaseResult<T, E> {
isOk(): boolean
isErr(): boolean
and<U>(res: Result<U, E>): Result<U, E>
}
And the two different implementations, one for Err
and one for Ok
.
class ErrImpl<E> implements BaseResult<never, E> {
readonly val!: E
constructor(val: E) {
this.val = val
}
isOk(): boolean {
return false
}
isErr(): boolean {
return true
}
and<U>(res: Result<U, E>): Result<U, E> {
return this
}
}
class OkImpl<T> implements BaseResult<T, never> {
readonly val!: T
constructor(val: T) {
this.val = val
}
isOk(): boolean {
return true
}
isErr(): boolean {
return false
}
and<U, E>(res: Result<U, E>) {
return res
}
}
Lastly, I create my Result
type as a union of the OkImpl
and ErrorImpl
.
export type Result<T, E> = OkImpl<T> | ErrImpl<E>
When I try testing the and
function:
function alwaysFailsString(): Result<boolean, string> {
return new ErrImpl("a")
}
let errString1 = alwaysFailsString()
let errString2 = alwaysFailsString()
let v = errString1.and(errString2)
I get a type error:
This expression is not callable.
Each member of the union type '(<U, E>(res: Result<U, E>) => Result<U, E>) | (<U>(res: Result<U, string>) => Result<U, string>)' has signatures, but none of those signatures are compatible with each other.ts(2349)
The rust documentation says that the and
function pub fn and<U>(self, res: Result<U, E>) -> Result<U, E>
:
Returns res
if the result is Ok
, otherwise returns the Err
value of self
.
My thought process is the following:
In BaseResult
, the function signature has a generic U
which is the new Ok
type the res Result
can take, and returns either res
, hence Result<U, E>
, or the Err
value of self, which is of type E
and thus part of Result<U, E>
.
In the ErrorImpl
, I return the Err
value (this
), which is a subset of Result<U, E>
, thus should work.
Lastly, in OkImpl
I return res
, which is of type Result<U, E>
thus should also work.
Where is the problem?
Minimal Reproducible Example:
interface BaseResult<T, E> {
val: T | E
and<U>(res: Result<U, E>): Result<U, E>
}
class ErrImpl<E> implements BaseResult<never, E> {
readonly val!: E
constructor(val: E) {
this.val = val
}
and<U>(res: Result<U, E>): Result<U, E> {
return this
}
}
class OkImpl<T> implements BaseResult<T, never> {
readonly val!: T
constructor(val: T) {
this.val = val
}
and<U, E>(res: Result<U, E>): Result<U, E> {
return res
}
}
export type Result<T, E> = OkImpl<T> | ErrImpl<E>
// Testing
function alwaysFailsString(): Result<boolean, string> {
return new ErrImpl("a")
}
let errString1 = alwaysFailsString()
let errString2 = alwaysFailsString()
let v = errString1.and(errString2)
The problem is that Result<T, E>
is a union and therefore the and()
method is a union of functions of the form (<U1, E1>(res: Result<U1, E1>) => Result<U1, E1>) | (<U2>(res: Result<U2, E>) => Result<U2, E>)
. Before TypeScript 3.3, you could only call unions of functions if each member of the union was identical. Then TypeScript added some support for calling unions, by figuring out how to unify the multiple signatures into a single signature. But this support is limited to situations where at most one of the functions is generic or overloaded. It's a lot of work to try to unify generic functions together and so TypeScript doesn't even really try. That's why your call fails: both members of the union are generic, and those generics are not identical to each other.
If you want to get this working, you need to work around that limitation. If you need the generics, then the only approach you can take is to make ErrImpl<E>
and OkImpl<T>
's and()
methods have identical call signatures. That's a little tricky because the ErrImpl<E>
call signature depends on E
, while the OkImpl<T>
doesn't. Instead let's look at the BaseResult<T, E>.and
's call signature and pull something useful out of it:
interface BaseResult<T, E> {
and<U>(res: Result<U, E>): Result<U, E>
}
So, from the point of view of and()
, this is the same as the following call signature using a this
parameter:
interface BaseResultNonGeneric {
and<T, U, E>(this: Result<T, E>, res: Result<U, E>): Result<U, E>
}
A this
parameter just says that the object on which a method is called will be of that type. The reason why we want to do this is to make sure the call signature doesn't actually need to care about the containing class type directly. Oh, and if we look at that signature, it sure seems like T
is useless, so we might as well replace it with any
:
interface BaseResultNonGeneric {
and<U, E>(this: Result<any, E>, res: Result<U, E>): Result<U, E>
}
And now that call signature can be copied down into both classes:
class ErrImpl<E> implements BaseResult<never, E> {
and<U, E>(this: Result<any, E>, res: Result<U, E>): Result<U, E> {
return this
}
class OkImpl<T> implements BaseResult<T, never> {
and<U, E>(this: Result<any, E>, res: Result<U, E>): Result<U, E> {
return res
}
}
Now that those call signatures are identical, you can call it even on a union:
let v = errString1.and(errString2) // okay