简体   繁体   English

TypeScript 泛型类型约束不能扣除类型

[英]TypeScript generic type constraints can't deduct type

I am playing with TypeScript type system and I seem to have hit an invisible wall.我正在玩 TypeScript 类型系统,我似乎碰到了一堵看不见的墙。

For starters, I have the Func helper type (since Function is not a generic type in TypeScript):对于初学者,我有Func辅助类型(因为Function在 TypeScript 中不是泛型类型):

type Func <A, B> = (_: A) => B;

Then I have a base abstract class.然后我有一个基础抽象类。 It seems it can't be an interface because then TypeScript won't allow me to specialize the methods' signatures, see the derived class below, which is dubious, but it works with abstract class and abstract override :似乎它不能是一个接口,因为 TypeScript 不允许我专门化方法的签名,请参阅下面的派生类,这是可疑的,但它适用于抽象类和abstract override

abstract class Wrappable <A> {
    abstract andThen <B>(func: Func<A, B>): Wrappable<B>;

    abstract andThenWrap <B>(func: Func<A, Wrappable<B>>): Wrappable<B>;
}

With the above definitions I can implement something like Maybe (also note the abstract override used to specialize the andThen and andThenWrap methods of the base class):通过上面的定义,我可以实现类似Maybe的东西(还要注意用于专门化基类的andThenandThenWrap方法的abstract override ):

abstract class Maybe <A> extends Wrappable <A> {
    abstract override andThen <B>(func: Func<A, B>): Maybe<B>;

    abstract override andThenWrap <B>(func: Func<A, Maybe<B>>): Maybe<B>;

    static option <A>(value: A | null | undefined): Maybe<A> {
        return (!value) ? Maybe.none<A>() : Maybe.some<A>(value);
    }

    static some <A>(value: A): Some<A> {
        return new Some<A>(value);
    }

    static none <A>(): None<A> {
        return new None<A>();
    }
}

class Some <A> extends Maybe <A> {
    private value: A;

    constructor(value: A) {
        super();

        this.value = value;
    }

    override andThen <B>(func: Func<A, B>): Maybe<B> {
        return new Some(func(this.value));
    }

    override andThenWrap <B>(func: Func<A, Maybe<B>>): Maybe<B> {
        return func(this.value);
    }
}

class None <A> extends Maybe <A> {
    constructor() {
        super();
    }

    override andThen <B>(_: Func<A, B>): Maybe<B> {
        return new None<B>();
    }

    override andThenWrap <B>(_: Func<A, Maybe<B>>): Maybe<B> {
        return new None<B>();
    }
}

The issues begin when I try to implement something more tricky, like this ExceptionW , which is supposed to wrap another wrapper class:当我尝试实现一些更棘手的东西时,问题就开始了,比如这个ExceptionW ,它应该包装另一个包装类:

class ExceptionW <R, W extends Wrappable<R>> extends Wrappable <W> {
    private value: W;

    constructor(value: W) {
        super();

        this.value = value;
    }

    override andThen <T, U extends Wrappable<T>>(func: Func<W, U>): ExceptionW<T, U> {
        return new ExceptionW<T, U>(func(this.value));
    }

    override andThenWrap <T, U extends Wrappable<T>>(func: Func<W, ExceptionW<T, U>>): ExceptionW<T, U> {
        return func(this.value);
    }
}

This gives me a super blurry errors:这给了我一个超级模糊的错误:

Property 'andThen' in type 'ExceptionW<R, W>' is not assignable to the same property in base type 'Wrappable<W>'.
  Types of parameters 'func' and 'func' are incompatible.
    Type 'B' is not assignable to type 'Wrappable<unknown>'.

and

Property 'andThenWrap' in type 'ExceptionW<R, W>' is not assignable to the same property in base type 'Wrappable<W>'.
  Types of parameters 'func' and 'func' are incompatible.
    Property 'value' is missing in type 'Wrappable<B>' but required in type 'ExceptionW<unknown, Wrappable<unknown>>'.

For what I can tell, it complains about the type parameter W which I try to constraint to be Wrappable<?> .据我所知,它抱怨我试图约束为Wrappable<?>的类型参数W

The second error misleads me to think it has something to do with the value property.第二个错误让我误以为它与value属性有关。 So I tried to mitigate it with yet another layer of abstraction:所以我尝试用另一层抽象来缓解它:

abstract class WrapperTransformer <A, W extends Wrappable<A>> extends Wrappable <W> {
    abstract override andThen <B, U extends Wrappable<B>>(func: Func<W, U>): WrapperTransformer<B, U>;

    abstract override andThenWrap <B, U extends Wrappable<B>>(func: Func<W, WrapperTransformer<B, U>>): WrapperTransformer<B, U>;
}

And again I see similar errors:我再次看到类似的错误:

Property 'andThen' in type 'WrapperTransformer<A, W>' is not assignable to the same property in base type 'Wrappable<W>'.
  Types of parameters 'func' and 'func' are incompatible.
    Type 'B' is not assignable to type 'Wrappable<unknown>'.

and

Property 'andThenWrap' in type 'WrapperTransformer<A, W>' is not assignable to the same property in base type 'Wrappable<W>'.
  Types of parameters 'func' and 'func' are incompatible.
    Call signature return types 'Wrappable<B>' and 'WrapperTransformer<unknown, Wrappable<unknown>>' are incompatible.
      The types of 'andThen' are incompatible between these types.
        Type '<B>(func: Func<B, B>) => Wrappable<B>' is not assignable to type '<B, U extends Wrappable<B>>(func: Func<Wrappable<unknown>, U>) => WrapperTransformer<B, U>'.
          Types of parameters 'func' and 'func' are incompatible.
            Types of parameters '_' and '_' are incompatible.
              Type 'B' is not assignable to type 'Wrappable<unknown>'.

At this stage I am a bit lost at why this is happening and I tend to think this might be either an issue with the type system, quirks of TypeScript being a transpiler to JavaScript or just me misunderstanding something about the TypeScript type system.在这个阶段,我对为什么会发生这种情况有点迷茫,我倾向于认为这可能是类型系统的问题,TypeScript 是 JavaScript 的转译器的怪癖,或者只是我对 TypeScript 类型系统的一些误解。

Can somebody please point me in the right direction?有人可以指出我正确的方向吗?

That's what I love about functional programming: you spend hours (or, as in my case, days) thinking about the problem and banging your head against the wall, but then you solve it with few neat lines of code.这就是我喜欢函数式编程的原因:您花费数小时(或者,在我的情况下,数天)思考问题并将头撞到墙上,然后用几行简洁的代码解决它。

TypeScript can not deduct the template type' constraints in this configuration. TypeScript 无法在此配置中扣除模板类型的约束。

But the bigger problem with the above snippet is actually not just the TypeScript itself, but the idea behind the code.但上述代码片段的更大问题实际上不仅仅是TypeScript 本身,而是代码背后的想法。

Just for the context: I was trying to implement few monads (namely: Maybe and Either ) and a monad transformer (in the question - ExceptionT ).只是为了上下文:我试图实现几个 monad(即: MaybeEither )和一个 monad 转换器(在问题中 - ExceptionT )。

With the code above, the implementation that just compiles could look like this:使用上面的代码,刚刚编译的实现可能如下所示:

class ExceptionW <A> implements Wrappable <A> {
    constructor(private readonly value: Wrappable<A>) {}

    andThen<B>(func: Func<A, B>): ExceptionW<B> {
        return new ExceptionW<B>(this.value.andThen(func));
    }

    andThenWrap<B>(func: Func<A, ExceptionW<B>>): ExceptionW<B> {
        return new ExceptionW<B>(this.value.andThenWrap(func));
    }
}

So instead of constraining the template parameter of ExceptionW , I just wrap the entire class around it.因此,我没有限制ExceptionW的模板参数,而是将整个类包裹在它周围。 Roughly put, utilizing composition over inheritance.粗略地说,利用组合而不是继承。

Done, case dismissed.完成,案件被驳回。


However, going deeper the rabbit hole, the idea behind ExceptionT is that we can use it as both the monad itself ( ExceptionT ) and use its map and flatMap (in the code above - andThen and andThenWrap , correspondingly) to operate on the value it wraps.然而,更深入的兔子洞, ExceptionT背后的想法是我们可以将它用作 monad 本身( ExceptionT )并使用它的mapflatMap (在上面的代码中 - andThenandThenWrap ,相应地)对它的值进行操作包裹。

Something like this would do:这样的事情会做:

class ExceptionW <A> implements Wrappable <A> {
    constructor(private readonly value: Func0<Wrappable<A>>) {}

    andThen<B>(func: Func<A, B>): ExceptionW<B> {
        return new ExceptionW<B>(() => this.value().andThen(func));
    }

    andThenWrap<B>(func: Func<A, ExceptionW<B>>): ExceptionW<B> {
        return new ExceptionW<B>(() => this.value().andThenWrap(func));
    }

    unsafeRun(): Wrappable<A> {
        return this.value();
    }
}

This implementation is a monad itself and it wraps the type A .这个实现本身就是一个 monad,它包装了A类型。

The implementation in the question, where the ExceptionW was wrapping the Wrappable<A> won't do.问题中的实现,其中ExceptionW包装Wrappable<A>不会做。 The reason for this is that the monad interface ( Wrappable in this case) enforces very specific rules for andThen and andThenWrap - they must operate on the type the monad wraps.这样做的原因是 monad 接口(在这种情况下为Wrappable )对andThenandThenWrap强制执行非常具体的规则 - 它们必须对 monad 包装的类型进行操作。

So if the class signature looks like所以如果类签名看起来像

class ExceptionW <A, W extends Wrappable<A>> implements Wrappable <W> {}

which is pretty much same as这与

class ExceptionW <A> implements Wrappable <Wrappable<A>> {}

then the methods inherited from the Wrappable interface should look like this:那么从Wrappable接口继承的方法应该如下所示:

andThen(func: Func<Wrappable<A>, Wrappable<B>>): ExceptionW<Wrappable<B>> {}

andThenWrap(func: Func<Wrappable<A>, ExceptionW<Wrappable<B>>>): ExceptionW<Wrappable<B>> {}

And the value of this monad would be... questionable.而这个单子的价值将是……值得怀疑的。


Thinking of how the monad would be used leads to the more reasonable implementation.考虑如何使用 monad 会导致更合理的实现。

Say, there is a function which takes an XML as a string, parses it and returns the parsed XMLDocument object:比如说,有一个函数将 XML 作为字符串,对其进行解析并返回解析后的XMLDocument对象:

const getResponseXML = (response: string): XMLDocument =>
    new DOMParser().parseFromString(response, "text/xml");

This logic can fail though, if the string passed is not a valid XML, hence we would normally wrap it in try..catch :但是,如果传递的字符串不是有效的 XML,则此逻辑可能会失败,因此我们通常会将其包装在try..catch中:

const getResponseXML = (response: string): XMLDocument => {
    try {
        const doc = new DOMParser().parseFromString(response, "text/xml");

        // see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling
        if (doc.querySelector('parsererror'))
            throw new Error('Parser error');

        return doc;
    } catch (e) {
        // ???
    }
};

In a functional way, one can use the Maybe<XMLDocument> monad for the simplest error handling or Either<Error, XMLDocument> for a more fine-grain control (and feedback) of the error case.在功能方面,可以使用Maybe<XMLDocument> monad 进行最简单的错误处理,或者Either<Error, XMLDocument>对错误情况进行更细粒度的控制(和反馈)。

Then the entire program would be built around one of those two monads:然后整个程序将围绕这两个 monad 之一构建:

const getResponseXML = (response: string): Either<Error, XMLDocument> => {
    const doc = new DOMParser().parseFromString(response, "text/xml");

    if (doc.querySelector('parsererror'))
        return Either<Error, XMLDocument>.left(new Error('Parser error'));

    return Either<Error, XMLDocument>.right(doc);
};

const program = getResponseXML('')
    .andThen(doc => processDocument(doc));

But that getResponseXML function is still an expression , not a value , meaning when you run the function, it will actually do some work instead of describe the intention to do the work (and the program won't be a chain of all the computations done to the work after it has been done sometime in the future ).但是那个getResponseXML函数仍然是一个表达式,而不是一个,这意味着当你运行这个函数时,它实际上会做一些工作而不是描述做工作的意图(并且程序不会是所有计算完成的链在未来某个时间完成之后的工作)。

This is uh-uh-no-good in functional programming world.这在函数式编程世界中是不好的。

Sounds like a perfect use case for something with the name ExceptionW .听起来像是一个名为ExceptionW的完美用例。

class ExceptionW <A> implements Wrappable <A> {
    constructor(private readonly task: Func0<Wrappable<A>>, private readonly exceptionHandler: Func<unknown, Wrappable<A>>) {}

    andThen<B>(func: Func<A, B>): ExceptionW<B> {
        return new ExceptionW<B>(
            () => this.task().andThen(func),
            (e) => this.exceptionHandler(e).andThen(func)
        );
    }

    andThenWrap<B>(func: Func<A, ExceptionW<B>>): ExceptionW<B> {
        return new ExceptionW<B>(
            () => this.task().andThenWrap(func),
            (e) => this.exceptionHandler(e).andThenWrap(func)
        );
    }

    runExceptionW(): Wrappable<A> {
        try {
            return this.task();
        } catch (e) {
            return this.exceptionHandler(e);
        }
    }
}

With that, the code becomes much more functional-programming-friendly:这样,代码变得对函数式编程更加友好:

const getResponseXML = (response: string): ExceptionW<XMLDocument> =>
    new ExceptionW(
        () => {
            const doc = new DOMParser().parseFromString(response, "text/xml");

            if (doc.querySelector('parsererror'))
                throw new Error('Parser error');

            Either<Error, XMLDocument>.right(doc)
        },
        (e) => Either<Error, XMLDocument>.left(e)
    );

So when you run the getResponseXML function, nothing will happen - you will simply get the ExceptionW<XMLDocument> object as a result.因此,当您运行getResponseXML函数时,什么都不会发生 - 结果您只会得到ExceptionW<XMLDocument>对象。 No parser will be created, no error returned.不会创建解析器,也不会返回错误。 Then you can write your program in a manner of "what will happen when we actually run this code and get some result":然后,您可以以“当我们实际运行此代码并获得一些结果时会发生什么”的方式编写程序:

const program = (getResponseXML('invalid XML')
    .andThen(doc => processXMLDocument(doc))
    .runExceptionW() as Either<Error, XMLDocument>>)
    .bimap(
        (result) => console.log('success', result),
        (error) => console.error('error', error)
    );

Just to make this a bit more prettier:只是为了让它更漂亮一点:

class ExceptionW {
    runExceptionW<W extends Wrappable<A>>(): W {
        try {
            return this.task() as W;
        } catch (e) {
            return this.exceptionHandler(e) as W;
        }
    }
}

const program = getResponseXML('invalid XML')
    .andThen(doc => processXMLDocument(doc))
    .runExceptionW<Either<Error, XMLDocument>>>()
    .bimap(
        (result) => console.log('success', result),
        (error) => console.error('error', error)
    );

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM