首页 > 解决方案 > 以类型安全的方式组合打字稿协同例程(生成器)

问题描述

概括

我正在编写一个“执行器”,它使用生成器函数来定义动作序列。

考虑到他们委托使用的 ActionSequences 的类型,我需要建议如何正确键入组合的 ActionSequences yield*。目前这似乎是不可能的。

从更简单的 ActionSequences 组合 ActionSequences 是我的方法的一个非常基本的要求。复杂的 ActionSequences 是由串联运行更简单的 ActionSequences 组成的,就像任何正常过程调用另一个 ActionSequences 一样。一些 ActionSequence 包装了其他序列,添加了一些行为。不幸的是,我只能在使用断言或类型断言绕过编译器时编译这些组合示例any,即使它们运行良好。

这个问题可能出现在其他组合生成器的情况下,但是组合 ActionSequences 的激励案例有助于定义困难并理解约束。

基本问题描述

必须键入一个外部生成器以接受它委托给 using 的所有内部生成器的 next() 值yield*。这很好。然而,yield*调用的使用需要内部生成器接受外部生成器的所有可能的 next() 值。这是不好的。您必须对编译器撒谎,即仅请求和使用数字的 ActionSequences 也会使用例如由其相邻序列类型产生的字符串。他们不处理这些,他们永远不必!

这个操场上有一个最小的复制品。似乎我需要一些缺失的语法或缺失的模式。任何人都可以提出一些建议吗?

背景细节

ActionSequence 是一个同步生成器。它产生动作(可能是异步的)并在执行这些动作后接受反应。这种方法使我能够将业务逻辑作为与其他所有内容分开的可读过程进行隔离和测试。

Action 和 ActionSequence 定义如下。Reaction 应该始终是严格类型的,但我经常不得不回退到any编译真实案例。

export interface Action<Reaction> { 
    act: () => Reaction | Promise<Reaction>; 
}

type ActionSequence<Ending,Reaction> = Generator<Action<Reaction>, Ending, Reaction>

该方法的核心是这样的循环。它只是运行生成器产生的每个动作,然后将结果返回给它“停止”的生成器。每个生成器只会从自己的 Actions 中真正获取类型化的返回值。

    while (!generated.done) {
      const reaction = await generated.value.act();
      generated = sequence.next(reaction);
    }

工作示例

countPlan 处理Reactions来自其AddActions. 最后一个数字Reaction也是返回的值,所以Reaction类型和Ending类型ActionSequence都是number...

function* countPlan(total:number) : ActionSequence<number, number>{
    let sum = 0;
    for(let i = 0; i < total; i++){
        sum = yield new AddAction(sum,1)
    }
    return sum
}

delayPlan 只产生一个延迟动作。延迟承诺以 undefined 解决,并且没有返回任何值,因此 Reaction 和 Ending 都是无效的......

function* delayPlan(delayMs: number): ActionSequence<void,void>{
    yield new DelayAction(delayMs)
}

现在,当我想将两个 ActionSequences countPlan() 和 delayPlan() 结合起来时,就会出现系统性问题。ActionSequence 没有合适的 Reaction 类型,它委托给两个 ActionSequence,一个接受number,另一个接受void

首先,我们组合的 ActionSequence 的 Reaction 必须合并voidnumber。那是因为 delayPlan() 有一个voidReaction 并且 countPlan() 有一个numberReaction,所以让我们将该 union 添加为 combinedPlan() 的 Reaction 定义。

function* combinedPlan<Ending, Reaction>(): ActionSequence<number, number|void> {
  yield* delayPlan(1000);
  const result = yield* countPlan(42);
  return result;
}

不幸的是,这些yield*行因编译错误而失败,因为单独的序列现在必须自己接受数字和无效。链接游乐场中针对 delayPlan 的编译器错误是...

Cannot delegate iteration to value because the 'next' method of its iterator expects type 'void', but the containing generator will always send 'number | void'.
  Type 'number' is not assignable to type 'void'

编译的版本如下所示。但是,请注意,必须将 Reaction 类型扩展为any。尽管信息存在并且定义明确,但我们实际上已经抛弃了所有类型检查。我们知道这个外部序列实际上只处理任何一个,number|void但不能这么说。

function* combinedPlan<Ending, Reaction>(): ActionSequence<number, any> {
  yield* delayPlan(1000);
  const result = yield* countPlan(42);
  return result;
}

我们可以对 ActionSequences 使用类型断言,但这只是绕过编译器的另一种方法。我不确定哪个更糟糕...

function* combinedPlan<Ending, Reaction>(): ActionSequence<number, number | void> {
  yield* delayPlan(1000) as ActionSequence<void, number | void>;
  const result = yield* countPlan(42) as ActionSequence<number, number | void>;
  return result;
}

是否有一种策略能够以一种原则性的方式通过委托调用来组合 ActionSequences,而无需通过使用或类型断言yield* countPlan(42)绕过编译器。any

标签: typescriptgeneratorcoroutineyieldtyping

解决方案


推荐阅读