“Typing the generator ‘chain’ function”

huangapple go评论68阅读模式
英文:

Typing the generator "chain" function

问题

我有以下函数:

function* chain(...iters) {
    for (let it of iters)
        yield* it
}

它接受一系列可迭代对象,并创建一个生成器,按顺序从每个对象中生成值。

我不确定如何正确地为支持混合类型的可迭代对象进行类型定义。如果我的输入是像 Iterable<X>Iterable<Y> 等,那么结果应该是 Iterable<X | Y>。如何为可变参数编写这个类型定义?

英文:

I've got the following function:

function* chain(...iters) {
    for (let it of iters)
        yield* it
}

It accepts a list of iterables and creates a generator that yields sequentially from each one.

I'm not sure how to type it correctly to support mixed-type iterables. If my inputs are like Iterable&lt;X&gt;, Iterable&lt;Y&gt; etc then the result should be Iterable&lt;X | Y&gt;. How to write this for a variadic argument?

Playground Link

答案1

得分: 4

以下是翻译好的部分:

See this GitHub issue for more info:
microsoft/TypeScript#41646 - 允许泛型 yield* 类型

通过使用受限制的泛型类型参数,生成器函数的返回类型可以从其参数中派生出来。

type Chain = <Iters extends readonly Iterable<unknown>[]>(
  ...iters: Iters
) => Iters[number];

在上面的函数签名中,泛型 Iters 必须能够分配给一个类型,该类型是不可变的 Iterable<unknown> 元素数组。因此,剩余参数 iters 中的每个参数都必须能够分配给 Iterable<unknown>。这将允许编译器推断每个可迭代参数的生成类型。

以下是将其应用于您展示的实现的示例,然后使用您的播放链接中的一些示例函数,以查看推断的返回类型:

declare function test1(): Iterable<number>;
declare function test2(): Iterable<string>;

const chain: Chain = function* (...iters) {
  for (let it of iters) yield* it;
};

const iter = chain(test1(), test2());
    //^? const iter: Iterable<number> | Iterable<string>;

for (const value of iter) {}
         //^? const value: string | number

您可以看到,推断的返回类型是 Iterable<number> | Iterable<string>,并且在 for...of 循环中使用它会产生一个生成类型,该生成类型是联合了联合中每个可迭代对象的生成类型。

我认为这已经根据您的问题标准产生了令人满意的结果,但实际的返回类型仍然可以改进以更准确地表示返回的可迭代对象本身。

通过使用一种类型工具(改编自链接的 GitHub 问题中的内容),可以从可迭代类型内部提取生成类型:

type YieldedFromIterable<
  I extends
    | Iterable<unknown>
    | Iterator<unknown>
    | IterableIterator<unknown>
    | Generator<unknown>
> = I extends
  | Iterable<infer T>
  | Iterator<infer T>
  | IterableIterator<infer T>
  | Generator<infer T>
    ? T
    : never;

...可以为函数创建一种替代返回类型:

type Chain = <Iters extends readonly Iterable<unknown>[]>(
  ...iters: Iters
) => Iterable<YieldedFromIterable<Iters[number]>>;

与第一个函数签名的返回类型相比(该返回类型是联合了每个可迭代参数的生成类型),这个返回类型是一个单一的可迭代对象,它生成了从每个参数派生的联合值 - 并且在使用它时看起来像这样:

const chain: Chain = function* (...iters) {
  for (let it of iters) yield* it as any;
};

const iter = chain(test1(), test2());
    //^? const iter: Iterable<string | number>;

for (const value of iter) {}
         //^? const value: string | number

在 TS Playground 中的代码


结束语:

在第二个示例中,不对第二个示例的生成值使用类型断言会导致编译器错误:


<details>
<summary>英文:</summary>

&gt; See this GitHub issue for more info:
&gt; [microsoft/TypeScript#41646 - Allow generic `yield*` types](https://github.com/microsoft/TypeScript/issues/41646)

By using a [constrained generic type parameter](https://www.typescriptlang.org/docs/handbook/2/generics.html#using-type-parameters-in-generic-constraints), the return type of your generator function can be derived from its arguments.

```lang-ts
type Chain = &lt;Iters extends readonly Iterable&lt;unknown&gt;[]&gt;(
  ...iters: Iters
) =&gt; Iters[number];

In the function signature above, the generic Iters must be assignable to a type that is a readonly array of Iterable&lt;unknown&gt; elements. So, each argument in the rest parameter iters must be assignable to Iterable&lt;unknown&gt;. This will allow the compiler to infer the yielded type of each iterable argument.

Here's an example of applying it to the implementation that you showed, and then using it with a couple of example functions from your playground link in order to see the inferred return type:

declare function test1(): Iterable&lt;number&gt;;
declare function test2(): Iterable&lt;string&gt;;

const chain: Chain = function* (...iters) {
  for (let it of iters) yield* it;
};

const iter = chain(test1(), test2());
    //^? const iter: Iterable&lt;number&gt; | Iterable&lt;string&gt;

for (const value of iter) {}
         //^? const value: string | number

You can see that the inferred return type is Iterable&lt;number&gt; | Iterable&lt;string&gt;, and that using it in a for...of loop produces a yielded value that is the union of the yield type of each iterable in the union.

I think this already produces a satisfactory result according to your question criteria, but the actual return type can still be improved to more accurately represent the returned iterable itself.

By using a type utility (adapted from content in the linked GitHub issue) which extracts the yielded type from within an iterable type:

type YieldedFromIterable&lt;
  I extends
    | Iterable&lt;unknown&gt;
    | Iterator&lt;unknown&gt;
    | IterableIterator&lt;unknown&gt;
    | Generator&lt;unknown&gt;
&gt; = I extends
  | Iterable&lt;infer T&gt;
  | Iterator&lt;infer T&gt;
  | IterableIterator&lt;infer T&gt;
  | Generator&lt;infer T&gt;
    ? T
    : never;

...an alternative return type can be created for the function:

type Chain = &lt;Iters extends readonly Iterable&lt;unknown&gt;[]&gt;(
  ...iters: Iters
) =&gt; Iterable&lt;YieldedFromIterable&lt;Iters[number]&gt;&gt;;

In juxtaposition with the first function signature's return type (which was a union of each of the iterable arguments), this one is a single iterable which yields a union value derived from each argument — and using it looks like this:

const chain: Chain = function* (...iters) {
  for (let it of iters) yield* it as any;
};

const iter = chain(test1(), test2());
    //^? const iter: Iterable&lt;string | number&gt;

for (const value of iter) {}
         //^? const value: string | number

Code in TS Playground


Concluding thoughts:

Not using a type assertion on the yielded value of the second example causes a compiler error:

const chain: Chain = function* (...iters) { /*
      ~~~~~
Type &#39;&lt;Iters extends readonly Iterable&lt;unknown&gt;[]&gt;(...iters: Iters) =&gt; Generator&lt;unknown, void, undefined&gt;&#39; is not assignable to type &#39;Chain&#39;.
  Call signature return types &#39;Generator&lt;unknown, void, undefined&gt;&#39; and &#39;Iterable&lt;YieldedFromIterable&lt;Iters[number]&gt;&gt;&#39; are incompatible.
    The types returned by &#39;[Symbol.iterator]().next(...)&#39; are incompatible between these types.
      Type &#39;IteratorResult&lt;unknown, void&gt;&#39; is not assignable to type &#39;IteratorResult&lt;YieldedFromIterable&lt;Iters[number]&gt;, any&gt;&#39;.
        Type &#39;IteratorYieldResult&lt;unknown&gt;&#39; is not assignable to type &#39;IteratorResult&lt;YieldedFromIterable&lt;Iters[number]&gt;, any&gt;&#39;.
          Type &#39;IteratorYieldResult&lt;unknown&gt;&#39; is not assignable to type &#39;IteratorYieldResult&lt;YieldedFromIterable&lt;Iters[number]&gt;&gt;&#39;.
            Type &#39;unknown&#39; is not assignable to type &#39;YieldedFromIterable&lt;Iters[number]&gt;&#39;.(2322) */
  for (let it of iters) yield* it;
};

I think this is caused by a limitation of TypeScript's control flow analysis (but perhaps I'm wrong about that and someone else can provide more clarity).

A solution is to assert the value's type, like this:

const chain: Chain = function* (...iters) {
  for (let it of iters) yield* it as Iterable&lt;YieldedFromIterable&lt;typeof iters[number]&gt;&gt;; // ok
};

// OR

const chain: Chain = function* (...iters) {
  for (let it of iters) yield* it as any; // ok
};

huangapple
  • 本文由 发表于 2023年2月6日 17:38:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/75359560.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定