首页 > 解决方案 > 经典的“省略”功能,类似的代码以通常的方式编写和使用柯里化编写时完全不同的打字结果,我错过了什么?

问题描述

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

经过长时间阅读堆栈溢出后,我想出了以下可行的解决方案(耶):

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 省略,然后返回一个函数,该函数将获取一个对象并在没有指定 props 的情况下返回它(对组合很有用)。

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

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 });根本没有推断出预期的类型:

来自 tsc 的意外推断类型,用于 fpOmit 的结果

我在这里做错了什么?

标签: typescriptcurryingtype-parameter

解决方案


调用泛型函数时,必须指定其所有类型参数;由调用者手动(如fn<MyObjType, MyKeyType>(...))或通过从传递给函数的参数推断(有时,从预期的返回类型的上下文中推断)。

在您的原始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]; }

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

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'),它的两个类型参数T和都D必须指定。但是,虽然编译器可以D'a'输入推断,但它根本不知道推断什么T,因此回退到约束

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()不依赖于传入它的对象的类型;无论如何{ [x: string]: unknown; }

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

所以我们不能这样做。


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

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()在 的类型中仍然是通用Tobj,因此返回的类型omitA()将取决于该类型:

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

Playground 代码链接


推荐阅读