使用泛型和重载输入求和类型时遇到问题

Trouble typing a sum type with generics and overloads

我正在尝试键入 Either 的实现,但我努力使这两个变体能够很好地相互配合。

一、实施:

type Unary<X, R> = (x: X) => R;
type Either<L=unknown, R=unknown> = Left<L> | Right<R>

class Right<R=unknown> {
    constructor (private value: R) {}

    static of <T>(value:T):Right<T> { return new Right(value); }

    map <U>(f:Unary<R, U>):Right<U> {
        return Right.of(f(this.value));
    }

    ap <T>(either: Left<T>): Left<T>
    ap <U>(either: Right<Unary<R, U>>): Right<U>
    ap <T, U>(either: Either<T, Unary<R, U>>): Either<T, U> {
        return either instanceof Left
            ? either
            : this.map(either.value);
    }
}

class Left<L=unknown> {
    constructor (private value: L) {}

    static of <T>(value:T):Left<T> { return new Left(value); }

    map (_:Function): Left<L> { return this; }

    ap <T>(either: Left<T>): Left<T>
    ap (either: Right): Left<L>
    ap (either: Either): Left { 
        return either instanceof Left ? either : this;
    }
}

从表面上看,一切似乎在运行时和编译时都运行良好:

const r1 = Right.of(1);
const r2 = Right.of(2);
const l1 = Left.of(Error('foo'));
const l2 = Left.of(Error('bar'));

const add = (a:number) => (b:number) => a + b

// Right { value: 3 } : Right<number>
const rr = r2.ap(r1.map(add));

// Left { value: [Error: foo] } : Left<Error>
const lr = r2.ap(l1.map(add));

// Left { value: [Error: bar] } : Left<Error>
const rl = l2.ap(r1.map(add));

// Left { value: [Error: foo] } : Left<Error>
const ll = l2.ap(l1.map(add));

...但如果我尝试定义提升函数,它就会崩溃。

const lift2 = <A, B, R>(f:(a: A) => (b: B) => R) => {
    function lifted(a:Right<A>, b:Right<B>): Right<R>
    function lifted<U>(a:Right<A>, b:Left<U>): Left<U>
    function lifted<T>(a:Left<T>, b:Right<B>): Left<T>
    function lifted<T>(a:Left<T>, b:Left): Left<T>
    function lifted(a: Either, b: Either):Either {
        return b.ap(a.map(f));
        //          ------=-
    }
    return lifted;
}

我有 2 个明显的错误

Argument of type 'Left<unknown> | Right<(b: B) => R>' is not assignable to parameter of type 'Left<unknown>'.
  Type 'Right<(b: B) => R>' is not assignable to type 'Left<unknown>'.
    Types have separate declarations of a private property 'value

The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.
Argument of type '(a: A) => (b: B) => R' is not assignable to parameter of type 'Unary<unknown, (b: B) => R> & Function'.
  Type '(a: A) => (b: B) => R' is not assignable to type 'Unary<unknown, (b: B) => R>'.
    Types of parameters 'a' and 'x' are incompatible.
      Type 'unknown' is not assignable to type 'A'.
        'A' could be instantiated with an arbitrary type which could be unrelated to 'unknown'

奇怪的是,在调用站点上推断出正确的类型:

const add = lift2(
    (a:number) => (b:number) => a + b
);

// Right { value: 3 } : Right<number>
const rr = add(r1, r2);

// Left { value: [Error: foo] } : Left<Error>
const lr = add(l1, r2);

// Left { value: [Error: bar] } : Left<Error>
const rl = add(r1, l2);

// Left { value: [Error: foo] } : Left<Error>
const ll = add(l1, l2);

首先,我建议使 lifted() 的实现签名更精确......也许是这样的:

function lifted<T, U>(
  a: Either<T, A>, 
  b: Either<U, B>
): Either<U, R> | Left<T> {...};

这应该可以解决任何涉及 unknown 的问题...您的 ab 的原始类型只是 Either,即 Either<unknown, unknown> (感谢 generic type parameter defaults),编译器抱怨你不能将 unknown 传递给需要 A.

的东西是正确的

事实上,我建议尽可能精确地制作完整的签名集,不要让 unknown 潜入任何不需要的地方:

    function lifted(a: Right<A>, b: Right<B>): Right<R>
    function lifted<U>(a: Right<A>, b: Left<U>): Left<U>
    function lifted<T, U>(a: Left<T>, b: Either<U, B>): Left<T>
    function lifted<T, U>(a: Either<T, A>, b: Either<U, B>): Either<U, R> | Left<T> {
        // impl to come
    }
    return lifted;

接下来,编译器不够聪明,看不到b.ap(a.map(f))是正确的,因为它把ab当成两个Either类型,它们是联合.虽然事实证明 b.ap(a.map(f)) 将始终有效,无论 abLeft 还是 Right,但 [= 没有单个调用签名32=] 或 Either.ap 这对编译器来说显然是正确的。 Either 是一个联合类型,因此它的 apmap 方法是函数的联合......和重载函数,引导。编译器无法以智能方式为所有这些重载联合解析调用签名。这类似于我一直在调用 correlated union types 时遇到的问题(参见 microsoft/TypeScript#30581)。

如果编译器要为 ab 的四种可能性中的每一种重新计算 b.ap(a.map(f)),它会对每一种都满意。但是因为你只有一行,所以编译器只考虑一次。没有诸如“control flow analysis which distributes over unions" (see microsoft/TypeScript#25051 这样的功能的封闭请求,如果它被实现,它会解决这个问题。)


没有这些能力,你有两个基本选择:

1:使用 type assertions 之类的东西告诉编译器不要担心该行的类型安全,例如:

return (b as any).ap(a.map(f)); // uses any here

这把类型安全的负担放在了你身上,所以你需要小心。不过,这是最方便的选择。

2:使用冗余代码让编译器遍历它能够验证类型安全的不同可能性:

const aMapF = a.map(f);
return aMapF instanceof Left ? b.ap(aMapF) :
  b instanceof Left ? b.ap(aMapF) :
    b.ap(aMapF);

这是完全类型安全的,编译器认为这是因为控制流分析允许将联合类型的事物缩小为单一类型的事物。当然,你为这种安全付出了冗余的重复冗余。

Playground link to code