最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin1浏览0评论

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

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

Share Improve this question edited Feb 3 at 20:02 Riccardo Perego asked Feb 3 at 19:39 Riccardo PeregoRiccardo Perego 5633 silver badges13 bronze badges 12
  • The problem is that you're trying to call a union of functions, and TS's support for unifying call signatures is known not to work when more than one union member is a generic or overloaded call signature. That addresses the question you asked, but presumably you also want to make it work. If that's important then I would like to see a minimal reproducible example without distractions, such as: what is T doing in BaseResult<T, E>? Nothing? Then remove it, please. Or edit to show what it does. – jcalz Commented Feb 3 at 19:49
  • The best you can do here if you want to call a Result<T, E>'s and() method is to make sure that both union members' and() call signature is identical. One approach is to add a this parameter as shown in this playground link. Does that meet your needs? If so I'll write an answer explaining; if not, what am I missing? – jcalz Commented Feb 3 at 20:04
  • @jcalz This does indeed meet my needs. Thanks again. – Riccardo Perego Commented Feb 3 at 20:08
  • I'd also appreciate if you could be able to make a comprehensive answer that explain why this works so that I can apply such insights on other more complex functions such as andThen. – Riccardo Perego Commented Feb 3 at 20:09
  • Upon further inspection this does not work when the second Result has a different Ok type as shown in this playground: tsplay.dev/wXAo8N @jcalz – Riccardo Perego Commented Feb 3 at 20:15
 |  Show 7 more comments

1 Answer 1

Reset to default 2

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

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论