经典的 "omit" 函数,相似的代码在用通常的方式编写和用柯里化编写时完全不同的输入结果,我错过了什么?

Classic "omit" function, similar code totally different typing results when written the usual way and when written with currying, what did I miss?

好的,所以我正在尝试编写一个类型感知的“省略”函数。

在对 stack-overflow 进行了长时间的阅读之后,我想出了以下可行的解决方案 (yay):

const omit = <
  T extends Record<string, unknown>,
  Del extends keyof T,
  U = { [Key in Exclude<keyof T, Del>]: T[Key] }
> (obj: T, ...props: Del[]): U =>
    Object.entries(obj).reduce((acc, [key, value]): U => {
      for (const del of props) {
        if (del === key) {
          return acc;
        }
      }
      return { ...acc, [key]: value };
    }, {} as U);

如果我写 omit({ a: 1, b: 2 }, 'a'); 那么 tsc 就很好理解了:

但我更喜欢用函数式编程的方式来编写这些东西,一个函数将 props 省略,然后 returns 一个函数将接受一个对象并 return 它没有指定的道具(对合成有用)。

所以我试着这样写,几乎是一样的代码:

const fpOmit = <
  T extends Record<string, unknown>,
  Del extends keyof T,
  U = { [Key in Exclude<keyof T, Del>]: T[Key] }
> (...props: Del[]) => (obj: T): U =>
    Object.entries(obj).reduce((acc, [key, value]): U => {
      for (const del of props) {
        if (del === key) {
          return acc;
        }
      }
      return { ...acc, [key]: value };
    }, {} as U);

没有错误,没有警告,但是这次调用 fpOmit('a')({ a: 1, b: 2 }); 根本没有推断出预期的类型:

我做错了什么?

调用generic函数时,必须指定其所有类型参数;由调用者手动(如 fn<MyObjType, MyKeyType>(...))或通过 inference 从传递给函数的参数(偶尔,根据预期的 return 类型)。

在您原来的 omit() 函数中:

declare const omit: <T extends Record<string, unknown>, D extends keyof T>(
    obj: T, ...props: D[]
) => { [K in Exclude<keyof T, D>]: T[K]; }

编译器可以从 objprops 参数中推断出 TD 类型参数,并且一切正常:

const result = omit({ a: 1, b: 2 }, "a");
// const result: {  b: number; }

但是在你的咖喱版本中:

declare const fpOmit: <T extends Record<string, unknown>, D extends keyof T>(
    ...props: D[]) => (obj: T) => { [K in Exclude<keyof T, D>]: T[K]; }

当你在一行中写下以下内容时:

const fpResult = fpOmit('a')({ a: 1, b: 2 });

还是一对函数调用,像这样:

const omitA = fpOmit('a');
const fpResult = omitA({ a: 1, b: 2 });

并且当你调用fpOmit('a')时,它的类型参数TD都必须指定。但是,虽然编译器可以从 'a' 输入中推断出 D,但它根本不知道要为 T 推断出什么,因此会退回到 constraint:

const omitA = fpOmit('a');
// const fpOmit: <Record<string, unknown>, "a">(
//   ...props: "a"[]) => (obj: Record<string, unknown>) => 
//  { [x: string]: unknown; }

// const omitA: (obj: Record<string, unknown>) => { [x: string]: unknown; }

一旦发生这种情况,一切就都结束了。 omitA() 的 return 类型不依赖于传递给它的对象的类型;无论如何都是{ [x: string]: unknown; }:

const fpResult = omitA({ a: 1, b: 2 });
// const fpResult2: { [x: string]: unknown; }

所以我们不能这样做。


您需要做的是更改泛型类型参数的 范围 ,以便只有在有足够的可用信息时才需要指定它们。所以T参数需要被fpOmit()移动到函数returned的调用签名中。这也意味着你必须重新表述你的约束;你必须用 D 来表达 T 而不是相反:

declare const fpOmit: <D extends PropertyKey>(
    ...props: D[]) => <T extends Record<D, unknown>>(
        obj: T) => { [K in Exclude<keyof T, D>]: T[K]; }

现在一切正常:

const fpResult = fpOmit('a')({ a: 1, b: 2 });
// const fpResult: { b: number; }

如果像以前那样把它拆开,你就明白为什么了:

const omitA = fpOmit('a');
// const fpOmit: <"a">(
//   ...props: "a"[]) => <T>(obj: T) => 
//   { [K in Exclude<keyof T, "a">]: T[K]; }

// const omitA: <T extends Record<"a", unknown>>(
//   obj: T) => { [K in Exclude<keyof T, "a">]: T[K]; }

fpResult() 编辑的函数 return 在 obj 的类型 T 中仍然是通用的,因此 omitA() 的 return 类型=] 将取决于该类型:

const fpResult2 = omitA({ a: 1, b: 2 });
// const fpResult2: { b: number }

Playground link to code