【问题标题】:How to type an array of generic functions in TypeScript如何在 TypeScript 中键入泛型函数数组
【发布时间】:2021-11-17 03:01:27
【问题描述】:

我有一些输入有问题。我能够将问题减少到以下最小示例。我有一个函数,其中第一个参数是通用的,并且被限制为可序列化为 JSON 的东西。

type Json = string | number | boolean | null | Json[] | { [property: string]: Json };
type Foo<A extends Json> = (n: A) => boolean;

const a: Foo<number> = (n: number) => n + 1 > 0;
const b: Foo<string> = (n: string) => n.endsWith("a");

我需要这些函数数组的类型定义,其中泛型部分可以是 Foo 的泛型部分允许的任何内容。

我明白为什么这不起作用。

const foos: Foo<Json>[] = [a, b]; // Error

而且,any 太松散了。它允许Foos 无效的值,例如此示例,其中函数的参数是HTMLElement,不能分配给Json

const foos: Foo<any>[] = [a, b, (n: HTMLElement) => true]; // Invalid, but no error

我需要的是这个数组中允许的Foos 的所有具体类型的联合。但是,该联合必须无限长才能覆盖任何可能等同于 Json 的内容。

type SomeFoo = Foo<string> | Foo<number> | ... | Foo<Record<string, Record<string, [number, string[]]>>> | ...;
const foos: SomeFoo[] = [a, b];

有没有办法使用泛型或其他机制来避免这种无限联合?

编辑:

如果有帮助,这里有一个稍微复杂一点的例子,它更接近于我试图输入的真实代码:playground。我有一个具有两个功能的对象。第一个函数的返回类型需要匹配第二个函数的第一个参数。

编辑 2:

根据来自 cmets 的反馈,对这个问题进行了重大重构,但它仍然提出相同的基本问题。

【问题讨论】:

  • 通用函数参数之间有什么共同特征吗?在我看来,您应该只使用any{},因为除了const 数组之外,您将无法确定成员函数接受的类型。也许你可以解释一下你打算如何使用它?
  • 不是typescript不能表达这个,而是这个type不太合理。想象一下你的沙箱中的代码可以工作,那么它会如何使用呢?例如我可以做b['a'].bar(123),这应该可以工作,因为类型签名Record&lt;string, MyType&lt;Json&gt;&gt;告诉我b['a']MyType&lt;Json&gt;类型,因此b['a'].bar(n: Json) =&gt; boolean类型,所以我应该可以通过number 给它,但实际上它只需要一个字符串。这是您需要绕过的问题,而不是打字稿在争论
  • 您的游乐场示例比您问题中的代码更令人回味;您可能希望将问题改写成这样。在任何情况下,您正在寻找的称为存在量化泛型(当您认为需要无限联合时,这就是您想要的)并且它们在 TS 中不受直接支持(请参阅ms/TS#14466 )。您可以模拟this 等类型。如果这符合您的需求,我可以写一个答案。
  • 另外,当你说你正在描述一个现有的 JavaScript 库时,你可以改变存在模拟器以适应这种情况,因为普遍量化的泛型(TS 和大多数其他语言中的“正常”泛型) ) 当您切换数据的消费者和提供者时,看起来存在量化。据推测,任何消耗事物数组的函数都可以成为普遍量化的泛型,从而减轻对数组本身进行存在量化的需求。但我需要一些激励minimal reproducible example 来提出具体建议。
  • 所以,就像this 一样,我可能会处理库类型;忘记尝试为数组类型编写无限联合,而让编译器推断出更强大的东西。然后以正常方式使processMyTypeArray() 泛型。让我知道您希望看到哪些版本的东西被写出来,最好是通过编辑问题中的代码来激发解决方案。一个玩具示例,但不是一个“最小”到无用的示例。

标签: typescript typescript-generics


【解决方案1】:

使这个特定示例难以讨论的一个问题是,从表面上看,(n: A) =&gt; boolean 类型的值foo(其中A 是一些未解决的generic 扩展Json 类型参数) 不能安全地调用。我不能打电话给foo(123),因为虽然123 extends Json,我需要知道123 extends A,我不知道。 A 对该函数的调用者来说是个谜。就好像该函数在说“我想要一些东西,但我不会告诉你它是什么”,除非你使用 the any typetype assertions,否则 TypeScript 的类型系统甚至不会让你猜测。在接下来的内容中,我将忽略这个问题,只使用类型断言,但如果这个问题有更多的动机(例如,像{val: A, consumer(val: A): void;} 这样的类型和未知的A 将是可用的,因为你总是可以安全地使用将对象的val 传递给它自己的consumer 方法)。哦,好吧。


无论如何,每当您发现自己希望拥有“无限的union type”时,这表明您想使用existentially-quantified generics。如果这些存在于 TypeScript 中,您可以编写如下内容:

// not valid TS, don't try this
type SomeFooArray = Array< <∃A extends Json> Foo<A> >;

&lt;∃A extends Json&gt;(...) 的意思是“existsA 可分配给包含表达式的 Json,但我不知道也不关心 A 是什么”。

不幸的是,与大多数具有泛型的语言一样,TypeScript 仅直接支持 universally-quantified generics,它充当无限 intersection,其中 &lt;A extends Json&gt;(...) 表示“封闭的表达式适用于 可分配给Json"的所有可能的 A 类型。

microsoft/TypeScript#14466 有一个针对存在类型的开放功能请求,但谁知道它是否会实现。


universal-vs-existential quantification 和 unions-vs-intersections 的一件事是它们彼此之间是duals,并且可以通过改变数据生产者和数据消费者的角色将一件事转化为对偶.因此,您可以通过将存在类型表示为类似Promise 的数据结构来模拟存在类型。我没有给你&lt;∃A extends Json&gt; Foo&lt;A&gt;,而是给你一个

type SomeFoo = <R>(cb: <A extends Json>(foo: Foo<A>) => R) => R;

这是一个将其Foo 保存在黑匣子中的函数,您可以将回调传递给它。该函数使用其Foo 作为参数调用回调并将返回结果返回给您。你可以用&lt;∃A extends Json&gt; Foo&lt;A&gt; 做的任何事情都可以用SomeFoo 做:

function acceptSomeFooArray(someFooArray: SomeFoo[]) {
    return someFooArray.map(someFoo => someFoo(foo => foo("hmm"))); // boolean, but
    // -------------------------------------------------> ~~~~~
    // '"hmm"' is assignable to the constraint of type 'A', 
    // but 'A' could be instantiated with a different subtype of constraint 'Json'.
}

哦,该死,有那个错误是因为你不能安全地调用它。很好:

function acceptSomeFooArray(someFooArray: SomeFoo[]) {
    return someFooArray.map(someFoo => someFoo(
        (<A extends Json>(foo: Foo<A>) => foo("hmm" as A))) // assert
    );
}

无论如何,将具体的Foo&lt;A&gt; 变成SomeFoo 很容易:

const toSomeFoo = <A extends Json>(foo: Foo<A>): SomeFoo => cb => cb(foo);

因此,您可以传递一个 SomeFoo 数组,其中需要一个 &lt;∃A extends Json&gt; Foo&lt;A&gt; 数组。只要您可以更改数据结构,这是一个可行的解决方案。


哦,但是您有一个无法更改其数据结构的现有库。在这种情况下,仍然可以使用生产者与消费者切换的替代方案。与其尝试提​​出一种特定类型,即“Foo&lt;A&gt; for some A”并将其应用于数据片段,不如使用任何需要处理此类数据并使其通用的库函数所以它可以为 all A 处理 Foo&lt;A&gt;

例如,如果您有一个库函数processFooArray 接受“一些”Foo 的异构数组,您可以这样声明:

type DistribFoo<T> = T extends Json ? Foo<T> : never;

// declaration 
declare function processFooArray<A extends readonly Json[]>(
    arr: readonly [...{ [K in keyof A]: DistribFoo<A[K]> }]): [...{ [K in keyof A]: boolean }];

在这里,DistribFoo&lt;T&gt; 采用 T extends Jsondistributes Foo&lt;T&gt; 的任意联合,因此 DistribFoo&lt;string | number&gt; 变为 Foo&lt;string&gt; | Foo&lt;number&gt;。这有助于防止 processFooArray() 采用无序数组类型。

processFooArray()A 的泛型,Json 元素的数组 类型。那么它接受的arr 值就是mapped array type。当您调用processFooArray(arr) 时,编译器可以从arr 中调用infer the A type,并使用它来验证数组的每个成员对于某些T 而言实际上是Foo&lt;T&gt;

请注意,我已将返回类型设置为与输入 arr 长度相同的 boolean 值数组,假设该函数将(不知何故!)调用每个函数并将每个函数映射到它的输出。

让我们测试一下:

const a: Foo<string> = (n) => n.endsWith("a")
const b: Foo<number> = (n) => n < 0.5
const c = (x: HTMLElement) => x.hasAttribute("src")
   
processFooArray([a, b]); // okay
processFooArray([a, b, c]); // error!
// ------------------> ~

看起来不错。编译器对[a, b] 很满意,但对[a, b, c] 不满意。

这种方法可能也有其局限性,特别是如果库函数产生值“Foo&lt;A&gt; for some A”;也许您可以根据库函数的输入找出 A 应该是什么,或者您可能只是随机选择一个 Anever 或其他东西。不过我不会在这条路上走得更远。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-02-27
    • 2019-01-23
    • 1970-01-01
    • 2019-05-15
    • 2021-06-15
    • 1970-01-01
    • 1970-01-01
    • 2020-11-15
    相关资源
    最近更新 更多