TypeScript 泛型类型约束不能推断类型



我正在玩TypeScript类型系统,我似乎碰到了一堵看不见的墙。

对于初学者,我有Func的助手类型(因为Function不是 TypeScript 中的泛型类型):

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

然后我有一个基本的抽象类。似乎它不能是一个接口,因为这样 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>;
}

通过上述定义,我可以实现类似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>();
}
}

当我尝试实现更棘手的东西时,问题就开始了,比如这个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);
}
}

这给了我一个超级模糊的错误:

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>'.

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>>'.

据我所知,它抱怨类型参数W我试图约束它Wrappable<?>

第二个错误误导我认为它与value属性有关。所以我试图用另一个抽象层来缓解它:

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

我再次看到类似的错误:

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>'.

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>'.

在这个阶段,我对为什么会发生这种情况有点迷茫,我倾向于认为这可能是类型系统的问题,TypeScript 是 JavaScript 的转译器的怪癖,或者只是我对 TypeScript 类型系统的一些误解。

有人可以指出我正确的方向吗?

这就是我喜欢函数式编程的原因:你花几个小时(或者,就像我的情况一样,几天)思考问题,然后用几行整齐的代码来解决它。

在此配置中,TypeScript 无法扣除模板类型的约束。

但上述代码片段更大的问题实际上不仅仅是 TypeScript 本身,而是代码背后的思想。

仅供上下文参考:我试图实现几个monad(即:MaybeEither)和一个monad转换器(在问题中 -ExceptionT)。

使用上面的代码,仅编译的实现可能如下所示:

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

因此,我没有约束ExceptionW的模板参数,而是将整个类包装在它周围。粗略地说,利用组合而不是继承。

完成,案件驳回。


然而,深入兔子洞,ExceptionT背后的想法是,我们可以将其用作monad本身(ExceptionT),并使用它mapflatMap(在上面的代码中 - 相应地andThenandThenWrap)对它包装的值进行操作。

像这样的事情就可以了:

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

此实现本身就是一个 monad,它将类型包装A

问题中的实现,即ExceptionW包装Wrappable<A>的位置是行不通的。这样做的原因是 monad 接口(在这种情况下Wrappable)对andThenandThenWrap强制执行非常具体的规则 - 它们必须在 monad 包装的类型上运行。

因此,如果类签名看起来像

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

这与

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

那么从Wrappable接口继承的方法应如下所示:

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

而这个单子的价值将是...可疑。


考虑如何使用monad会导致更合理的实现。

比如说,有一个函数将XML作为字符串,解析它并返回解析的XMLDocument对象:

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

但是,如果传递的字符串不是有效的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) {
// ???
}
};

在功能方面,可以使用Maybe<XMLDocument>monad 进行最简单的错误处理,也可以使用Either<Error, XMLDocument>对错误情况进行更精细的控制(和反馈)。

然后整个程序将围绕这两个monads之一构建:

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

但是getResponseXML函数仍然是一个表达式,而不是一个值,这意味着当你运行函数时,它实际上会做一些工作,而不是描述做这项工作的意图(并且程序不会是完成工作后对工作所做的所有计算的链)。

这在函数式编程世界中是呃-呃-不好的。

听起来像是名为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);
}
}
}

这样,代码就变得更加函数式编程友好:

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

因此,当您运行getResponseXML函数时,什么都不会发生 - 结果您将简单地获取ExceptionW<XMLDocument>对象。不会创建解析器,也不会返回任何错误。然后,您可以按照"当我们实际运行此代码并获得一些结果时会发生什么"的方式编写程序:

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

只是为了让它更漂亮一点:

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

最新更新