In the following code, I have a Result
generic type that is a union between an error implementation ErrImpl
and an OK implementation OkImpl
. If a result is definitely Ok
, it will take the form of Result<T, never>
, while if it is definitely an error it will take the form of Result<never, E>
. If it takes the shape Result<T, E>
it can be either Ok
or Err
.
I want to implement an and<U>(res: Result<U, E>): Result<U, E>
function such that if this
is Ok
it returns res
, otherwise, if this
is Err
, it returns this
.
If this
and res
take the form of Result<T, E>
and Result<U, E>
(meaning that they aren't definitely either Ok
or Err
), the return type is simply Result<U, E>
. The complication comes if one of the two E
's is never
, in that case the return type has E
as the non-never
, E
, unless they are both never
which in that case it is never
too.
I achieved the desired result by implementing function overloads, and it works fine. The thing I want to understand is why I have to repeat the second signature twice for it to work. If I don't repeat it twice the type system only considers the first overload.
Here is the code:
class ErrImpl<E> {
readonly val: E
readonly isError = true;
constructor(val: E) {
this.val = val
}
and<E, U>(this: Result<any, never>, res: Result<U, E>): Result<U, E>
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E>
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E> {
return this as Result<U, E>
}
}
class OkImpl<T> {
readonly val: T
readonly isError = false;
constructor(val: T) {
this.val = val
}
and<E, U>(this: Result<any, never>, res: Result<U, E>): Result<U, E>
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E>
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E> {
return res
}
}
export type Result<T, E> = OkImpl<T> | ErrImpl<E>
export const Ok = <T>(val: T): Result<T, never> => { return new OkImpl(val) }
export const Err = <E>(val: E): Result<never, E> => { return new ErrImpl(val) }
The two lines
and<E, U>(this: Result<any, never>, res: Result<U, E>): Result<U, E>
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E>
without a function implementation are called overload signatures and
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E> {
return this as Result<U, E>
}
is the function implementation with an implementation signature. Adding semicolons makes it clearer:
class ErrImpl<E> {
readonly val: E
readonly isError = true;
constructor(val: E) {
this.val = val
}
and<E, U>(this: Result<any, never>, res: Result<U, E>): Result<U, E>;
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E>;
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E> {
return this as Result<U, E>
}
}
class OkImpl<T> {
readonly val: T
readonly isError = false;
constructor(val: T) {
this.val = val
}
and<E, U>(this: Result<any, never>, res: Result<U, E>): Result<U, E>;
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E>;
and<E, U>(this: Result<any, E>, res: Result<U, NoInfer<E>>): Result<U, E> {
return res
}
}
The overload signatures end with ;
. The function implementations end with the function body in curly braces and the implementation signature ends before the function body.
The implementation signature doesn't count as overload signature, because TypeScript allows only one function implementation for all signatures, and you usually need broader types and optional arguments for the implementation that you don't want to allow as signature for the function call.
You can find it in the documentation. The last signature isn't allowed.