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

时间:2019-12-11 17:12:41

标签: typescript overloading typescript-generics either

我试图弄清楚如何在任一monad上实现辅助.flatMap方法。与常规.flatMap不同,它接受一个lambda,该lambda返回一些常规值而不是Either实例。它应该像适配器一样工作,以使用不是为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);

更新:出于好奇,更多“封装”的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行

1 个答案:

答案 0 :(得分:1)

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

由于类型f的任何(value: TResult) => Either<R, E>也会 成为类型(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