Search code examples
typescript

TypeScript: Implementing Rust-Like Result Type and Resolving Type Inference Issues in and Method


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?

Playground

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)

Minimal Reproducible Example Playground


Solution

  • 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
    

    Playground link to code