Curried 时 TypeScript 泛型不适用于先前的函数,如何?

TypeScript Generics Don't Apply To Previous Function When Curried, How To?

我有一个处理 promise 的 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

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

我将使用以下声明:

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 类型,因为 data 是输入 Foo[]。有这样的东西 contextual typing,其中编译器将推断类型 "backwards in time",通过查看某物的使用方式并弄清楚它 应该被声明为什么类型 让它工作。不幸的是,编译器通过多个函数调用向后执行此操作有点过分。好吧。


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

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 => x.name 中的意图是 x 应该是 Foo,您将需要通过类型注释将该意图传达给编译器:

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

这是我针对您提出的问题推荐的解决方案;您不需要开发人员在调用 mapP() 时手动指定 TR。相反,您正在注释回调的参数,以便编译器 可以 推断 TR 本身,它确实这样做了。

请注意,通过使用 generic 回调,您甚至可以变得更花哨并进行交流 "I'd like the callback to apply to anything with a name property and return a value of that type":

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()本身就是一个泛型函数。


因此,备份:如果您的用例真的是通过调用它而不保留对它的引用来立即使用部分应用的函数,那么您应该在不使用柯里化的情况下执行此操作。 "best of both worlds" 方法可能是一个重载的混合函数,既有柯里化的也有非柯里化的:

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