Search code examples
typescript

Why do I have to repeat function signature in overload twice for it to work?


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) }

Playground


Solution

  • 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.