Выяснить правильные типы - перегрузка метода интерфейса Typescript - PullRequest
0 голосов
/ 11 декабря 2019

Я пытаюсь выяснить, как реализовать вспомогательный метод .flatMap для любой монады. В отличие от обычного .flatMap, он принимает лямбду, которая возвращает какое-то обычное значение вместо экземпляра Either. Он должен работать как адаптер для использования функций, которые не были предназначены для использования с Either.

В примере ниже я назвал этот метод адаптера .flatmap. Может кто-нибудь предложить, как я могу преобразовать эту функцию в допустимую перегрузку Typescript обычной .flatMap?

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

Ссылка на игровую площадку. Использование перегрузки - строки # 74 и # 76

...