【问题标题】:TypeScript Generics Don't Apply To Previous Function When Curried, How To?TypeScript 泛型在 Curried 时不适用于以前的函数,如何?
【发布时间】:2020-02-13 00:12:56
【问题描述】:

我有一个处理承诺的map 的咖喱版本。它需要两个参数,一次一个。它需要两个类型参数来让调用者提供无法推断的片段。然而,实际上调用者总是必须提供它们,因为类型不是在它们传入的转换函数中推断出来的。函数看起来像这样:

/**
 * @description
 *   Like `ramda.map`, but handles an iterator that returns a promise (or not).
 *
 * @example
 *   await mapP((x: number) => Promise.resolve(x + 1))([1, 2, 3]) // -> [2, 3, 4]
 */
export const mapP = <T, R>(xf: (value: T) => Promise<R> | R) => (
  data: T[],
): Promise<R[]> => pipe(when(isNil, always([])), map(xf), allP)(data)

这是我调用它,你可以看到 x 不为类型系统所知。

如何解决我编写函数以使类型工作的方式(不放弃柯里化 - 我知道如果不柯里化它会解决的)?

【问题讨论】:

  • 这可能不是这种情况,但你能确保它不会编译。我想确保不只是 IDE 的类型推断不好
  • 是的,确认tsc 显示相同的错误。
  • 请将代码发布为文本,而不仅仅是图像(有关更多信息,请参阅How to Ask)。我希望能够将其复制并粘贴到独立的 IDE 中以提出答案,但现在我不能。 minimal reproducible example 对获得好的答案大有帮助。祝你好运!
  • 另外,鉴于有人应该能够编写const f = mapP(x =&gt; x.name),然后是f(data),你会如何建议编译器知道xFoo 类型? mapP((x: Foo)=&gt;x.name)(data) 是否满足您的需求?这里的主要问题似乎是上下文类型推断没有从 data 向后延伸到上一个函数调用中的 x 类型。
  • @jcalz 是的,我的想法完全正确。我不知道在前一种情况下它是如何知道的,但希望有一些我不知道的魔法。

标签: typescript functional-programming currying


【解决方案1】:

我将使用以下声明:

declare const mapP: <T, R>(xf: (value: T) => Promise<R> | R) => (
  data: T[],
) => Promise<R[]>;

这与您的版本相同,但不担心实施。无论如何,您遇到了以下问题,无法在回调中推断 x 的类型:

const res = mapP(x => x.name)(data); // error!
// x is unknown ----> ~

期望编译器可能推断x 必须是Foo 类型并不是完全不合理的,因为dataFoo[] 类型。像contextual typing 这样的事情,编译器将通过查看某事物的使用方式并确定它应该被声明为什么类型应该被声明为,从而“在时间上向后”推断类型去工作。不幸的是,编译器通过多个函数调用向后执行此操作有点过分。哦,好吧。


在我看来,柯里化的最大吸引力在于能够部分应用一个函数,然后稍后使用该部分应用的函数,如下所示:

const f = mapP(x => x.name); // error!
// x is unknown --> ~

// later

const res2 = f(data);

在这种情况下,期望编译器知道关于x 的任何有用信息是难以置信的,尤其是考虑到其他可能的调用,例如:

const res3 = f([{ name: false }]);

其中x 应该是{name: boolean} 而不是Foo。如果您在 x =&gt; x.name 中的意图是 x 应该是 Foo,则您需要通过类型注释将该意图传达给编译器:

const res4 = mapP((x: Foo) => x.name)(data); // okay Promise<string[]>

如前所述,这是我建议您解决问题的解决方案;当您调用mapP() 时,您不需要开发人员手动指定TR。相反,您正在注释回调的参数,以便编译器可以推断TR 本身,它会这样做。

请注意,您甚至可以通过使用 通用回调:

const g = mapP(<T>(x: { name: T }) => x.name);

const res5 = g(data); // Promise<string[]>;
const res6 = g([{ name: false }]); // Promise<boolean[]>;

这里编译器可以使用它在TS3.4中引入的一些higher order type inference来查看g()本身就是一个泛型函数。


所以,备份:如果您的用例确实是通过调用它而不持有对它的引用来立即使用部分应用的函数,那么您可能应该在不使用curry的情况下这样做。 “两全其美”的方法可能是一个重载的混合函数,它既是柯里化的又是非柯里化的:

function mapQ<T, R>(xf: (value: T) => Promise<R> | R, data: T[]): Promise<R[]>;
function mapQ<T, R>(xf: (value: T) => Promise<R> | R): (data: T[]) => Promise<R[]>;
function mapQ<T, R>(
  xf: (value: T) => Promise<R> | R,
  data?: T[]
): ((data: T[]) => Promise<R[]>) | Promise<R[]> {
  return data ? mapQ(xf)(data) : mapQ(xf);
}

然后,当且仅当您的意图是稍后使用该功能时,您才可以使用它:

const res7 = mapQ(x => x.name, data); // okay Promise<string[]>
const h = mapQ((x: Foo) => x.name);
const res8 = h(data); // okay Promise<string[]>

好的,希望对您有所帮助;祝你好运!

Playground link to code

【讨论】:

  • 非常感谢您的评论!!我不知道上下文输入(至少明确知道),这是一个很好的快速阅读!是的,我的大多数用例是当转换函数不知道正在操作的data 时,但在某些情况下是这样,我希望我能找到一种方法让 TS 弄清楚。是的,自动柯里化方法是理想的,但通常与我编写的所有函数无关。在两个 args 的情况下,它并没有那么糟糕,但会变得更糟。我还没有找到一个运行良好的curry 函数(ramda 或其他)。你知道吗?
  • 感谢深思熟虑的文章。
  • @jcalz &lt;T&gt;(x: { name: T }) 正是 TS 应该推断的函数参数的 x 的类型。如果您想专门化为更具体的类型,您应该只需要显式输入x
  • @bob 自动推断具体函数的泛型类型对于编译器来说可能非常昂贵;每个未注释的参数都会变成泛型,携带这些泛型几乎肯定会影响性能。我也不确定是否会有很多支持让编译器变得如此宽松。论点“x =&gt; x.name 不应该是错误,x 应该被推断为使它不是错误的东西”也意味着let x = "hello"; x = 3; 应该没有错误并且x 应该被推断为string | number
  • 关于宽容的后一点可能在this comment of microsoft/TypeScript#15114 中得到最好的总结。
猜你喜欢
  • 1970-01-01
  • 2016-03-13
  • 1970-01-01
  • 1970-01-01
  • 2023-02-25
  • 2023-02-11
  • 1970-01-01
  • 2018-08-30
  • 1970-01-01
相关资源
最近更新 更多