找出正确的类型 - Typescript 接口方法重载

Figure out right types - Typescript interface method overload

我正在尝试弄清楚如何在 Either monad 上实现辅助 .flatMap 方法。与常规 .flatMap 不同,它接受 returns 一些常规值而不是 Either 实例的 lambda。它应该像适配器一样工作,以使用并非设计用于 Either 的功能。

在下面的示例中,我将此适配器方法命名为 .flatmap。谁能建议我如何将该函数转换为常规 .flatMap 的合法 Typescript 重载?

interface Either<TResult, TError> {

  flatmap<R>(f: (value: TResult) => R): Either<R, void | TError>;

  flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
}

class Left<TResult, TError extends { type: any }> implements Either<TResult, TError> {

  public constructor(private readonly error: TError) {}

  public flatmap<R>(f: (value: TResult) => R): Either<R, void | TError> {
    return this.flatMap(n => new Left(this.error));
  }

  public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError> {
    return new Left(this.error);
  }
}

class Right<TResult, TError> implements Either<TResult, TError> {

  public constructor(private readonly value: TResult) { }

  public flatmap<R>(f: (value: TResult) => R): Either<R, void | TError> {

    return new Right(f(this.value));
  }

  public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError> {

      return f(this.value);
  }
}

class ResourceError {
  type = "ResourceError" as const

  public constructor(public readonly resourceId: number) {}
}

class ObjectNotFound { 
  type = "ObjectNotFound" as const

  public constructor(public readonly msg: string, public readonly timestamp: string) { }
}

class DivisionByZero {
  type = "DivisionByZero" as const
}

function f1(s: string): Either<number, DivisionByZero> {
  return new Right(+s);
}

function f2(n: number): Either<number, ResourceError> {
  return new Right(n + 1);
}

function f3(n: number): Either<string, ObjectNotFound> {
  return new Right(n.toString());
}

function f5(n: number): number {
  return n * 10;
}

function f6(s: string): string {
  return s + '!';
}

const c = f1('345')
  .flatMap(n => f2(n))
  .flatmap(n => f5(n))
  .flatMap(n => f3(n))
  .flatmap(n => f6(n));

console.log(c);

更新: 出于好奇,更多 'encapsulated' api + 静态检查模式匹配:

type UnionOfNames<TUnion> = TUnion extends { type: any } ? TUnion["type"] : never

type UnionToMap<TUnion> = {
  [NAME in UnionOfNames<TUnion>]: TUnion extends { type: NAME } ? TUnion : never
}

type Pattern<TResult, TMap> = {
  [NAME in keyof TMap]: (value: TMap[NAME]) => TResult
}

type Matcher<TUnion, TResult> = Pattern<TResult, UnionToMap<TUnion>>;

interface Either<TResult, TError> {

  flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;

  flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;

  match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R;
}

abstract class Either<TResult, TError> {

  public static Right<TResult>(value: TResult): Either<TResult, never> {

    return new Either.rightClass(
      value,
      x => x instanceof Either.leftClass || x instanceof Either.rightClass,
      Either.Right
    );
  }

  public static Left<TError extends { type: any }>(error: TError): Either<never, TError> {

    return new Either.leftClass(error);
  }

  private static readonly leftClass =
    class <TResult, TError extends { type: any }> implements Either<TResult, TError> {

      public constructor(private readonly error: TError) { }

      public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
      public flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
      public flatMap(f: (value: TResult) => any) {

        return this;
      }

      public match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R {

        return (error as any)[this.error.type](this.error);
      }
    };

  private static readonly rightClass =
    class <TResult, TError> implements Either<TResult, TError> {

      public constructor(
        private readonly value: TResult,
        private readonly isEitherInst: (x: any) => boolean,
        private readonly rightFactory: <R>(result: R) => Either<R, TError>
      ) { }

      public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
      public flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
      public flatMap(f: (value: TResult) => any) {

        const result = f(this.value);

        return this.isEitherInst(result) ? result : this.rightFactory(result);
      }

      public match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R {

        return success(this.value);
      }
    }
}

class ResourceError {
  type = "ResourceError" as const

  public constructor(public readonly resourceId: number) { }
}

class ObjectNotFound {
  type = "ObjectNotFound" as const

  public constructor(public readonly msg: string, public readonly timestamp: string) { }
}

class DivisionByZero {
  type = "DivisionByZero" as const
}

class NetworkError {
  type = "NetworkError" as const

  public constructor(public readonly address: string) {}
}

class GenericError {
  type = "GenericError" as const

  public constructor(public readonly exception: Error) {}
}

function f1(s: string): Either<number, DivisionByZero> {
  console.log('f1()');
  return Either.Right(+s);
}

function f2(n: number): Either<number, ResourceError> {
  console.log('f2()');
  return Either.Right(n + 1);
}

function f3(n: number): Either<string, ObjectNotFound> {
  console.log('f3()');
  return Either.Right(n.toString());
  //return Either.Left(new ObjectNotFound('not found ', Date.now().toString()));
}

function f4(s: string): number { 
  console.log('f4()');
  return +s * 10;
}

function f5(s: number): Either<string, ResourceError> {
  console.log('f5()');
  return Either.Right(s.toString() + '!');
}

const c = f1('345')
  .flatMap(f2)
  .flatMap(f3)
  .flatMap(f4)
  .flatMap(f5);


const r = c.match(
  (result: any) => result.toString(),
  {
    //GenericError: (value: GenericError) => value.exception.message,
    ObjectNotFound: (value: ObjectNotFound) => value.msg + value.timestamp,
    ResourceError: (value: ResourceError) => 'resourceError',
    DivisionByZero: (value: DivisionByZero) => 'divisionByZero',
  }
);

console.log(r);

Link去游乐场。重载的使用 - #74 & #76

overload 调用签名应该按照从最严格到最不严格的顺序排列,所以我可能会这样做:

interface Either<TResult, TError> {
  flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
  flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
}

因为 (value: TResult) => Either<R, E> 类型的任何 f(value: TResult) => R 类型的值(对于不同的 R ), 但反过来就不行了,我们只好这样搞了。如果我们翻转顺序,那么任何调用都不会被视为 => Either<R, E> 调用签名,因为它始终匹配更通用的 => R 签名。


实现重载需要一个单一的实现签名(因为在运行时只有一个函数),所以你需要编写运行时代码来弄清楚在处理不同类型的 f 时在运行时要做什么。

您的 Left 实现似乎完全独立于 f,因此实现起来很容易:

class Left<TResult, TError extends { type: any }> implements Either<TResult, TError> {

  public constructor(private readonly error: TError) { }

  public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
  public flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
  public flatMap(f: (value: TResult) => any) {
    return new Left(this.error);
  }
}

至于Right的实现,我们需要在运行时判断f(this.value)是否构成Either。假设只有 LeftRight 类 实现 Either,我会这样做:

class Right<TResult, TError> implements Either<TResult, TError> {

  public constructor(private readonly value: TResult) { }
  public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
  public flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
  public flatMap(f: (value: TResult) => any) {
    const ret = f(this.value);
    return ret instanceof Left || ret instanceof Right ? ret : new Right(ret);
  }
}

这似乎在您的示例代码中表现得很好:

const c = f1('345').flatMap(f2).flatMap(f5).flatMap(f3).flatMap(f6);
console.log(c); // Object { value: "3460!" }

好的,希望对您有所帮助;祝你好运!

Link to code