【问题标题】:How to restrict a generic type of a method to an object in typescript?如何将方法的泛型类型限制为打字稿中的对象?
【发布时间】:2021-11-25 03:15:32
【问题描述】:

我一直在尝试为我的方法提出一个泛型类型,并将该类型限制为具有以下要求的对象。

下面是我正在使用的函数。然而,这个函数是在 React 应用程序的自定义钩子中使用的。

function useCustomHook<T>(initialData: T[]) {
  const [changes, setChanges] = React.useState<T[]>([])

  // calling the method inside the hook with an element should be retrieved from changes
  doSomething() 
}

function doSomething<T>(obj: T) {
  Object.entries(obj).forEach(([key, value]) => {
    console.log(key, value)
  })
}

// Type
type ExampleType = {
  property1: number,
  property2: string
}
// Interface
interface ExampleInterface {
  property1: number;
  property2: string;
}

它应该为以下示例抛出错误(基本上它不应该接受任何原始类型,如字符串、数字、布尔值、null、未定义......):

doSomething('test')
doSomething(12)
doSomething(undefined)

它应该接受以下示例:

doSomething({})
doSomething({ property1: 12, property2: 'string'})


doSomething<ExampleType>({ property1: 12, property2: 'string'})
doSomething<ExampleInterface>({property1: 12, property2: 'string'})

我已经尝试过像这样更改方法:
函数 doSomethingextends Record>

它适用于大多数示例(意味着它会引发错误),但在使用接口时它不起作用(抛出索引签名缺失)。将现有接口更改为类型不是解决方案,我认为如果示例函数是库的一部分,我作为库的用户希望同时拥有接口和类型这两个选项。

但是,如果我将 unknown 更改为 any 但我相信在 Typescript 中应该避免使用 any。

如果有任何建议,我将不胜感激。我相信一定有办法实现它。这里是沙盒:https://codesandbox.io/s/sweet-wildflower-hqr1t

【问题讨论】:

    标签: typescript react-hooks typescript-generics


    【解决方案1】:

    这里正确的做法是将constrain类型参数T改为the object type,具体意思是“一个不是原始类型的类型”:

    function doSomething<T extends object>(obj: T) {
      Object.entries(obj).forEach(([key, value]) => {
        console.log(key, value)
      })
    }
    

    您可以验证它是否以这种方式工作:

    doSomething('test'); // error
    doSomething(12); // error
    doSomething(undefined); // error
    doSomething({}) // okay
    doSomething({ property1: 12, property2: 'string'}) // okay
    doSomething<ExampleType>({ property1: 12, property2: 'string'}) // okay
    doSomething<ExampleInterface>({property1: 12, property2: 'string'}) // okay
    

    这正是 object 类型在 TypeScript 中的用途,以及它存在的原因。


    现在,在这一点上,我希望得到响应,当您使用默认配置的 ESLint's ban-types rule 时,它会抱怨 object 并带有以下形式的警告:

    避免使用object 类型,因为目前由于无法断言密钥存在而难以使用。见microsoft/TypeScript#21732

    这条规则在某些情况下可能是善意和有用的,object 确实有一些缺点,但正如您所见,Record&lt;string, unknown&gt; 并不总是一种改进。某些东西“难以使用”大概并不意味着应该完全禁止它以支持其他东西,特别是如果其他东西不适用于该用例。用刀打开罐头很难,你应该改用开罐器,但这并不意味着你应该尝试用开罐器切面包。不同的类型有不同的用例。并且上面代码中的T extends object 似乎是 100% 适合这项工作的工具。

    由于我是提交microsoft/TypeScript#21732 的人,我可能对这个问题有点过度情绪化,我希望看到key in obj 充当obj 的类型守卫,断言属性存在。但是that issue absolutely does not mean that object is useless,看到其他项目中的所有这些 GitHub 问题都与此相关联,这是他们仔细从代码中删除 object 的原因,这有点令人筋疲力尽。

    哦,好吧!

    Playground link to code

    【讨论】:

    • 我想你刚刚说服我在我的代码库中取消这条规则。这不像我们在这里谈论any。这里还有一个遗漏的案例,不确定 OP 的用例是否需要,是一个数组。
    • 数组不是原始数组,所以我不确定我们为什么要排除它(尽管像普通对象一样遍历数组有时会做一些奇怪的事情)。
    【解决方案2】:

    我提供这个答案,但遗憾的是,我不能 100% 确定为什么会这样。

    但是,如果您不限制泛型并设置参数类型:

    T & Record<string, unknown>
    

    然后它会按您的预期工作。

    function doSomething<T>(obj: T & Record<string, unknown>) {
      Object.entries(obj).forEach(([key, value]) => {
        console.log(key, value)
      })
    }
    

    Playground


    假设:

    当您推断一个泛型参数时,它必须完全满足该约束。在这种情况下,Record 意味着在interface 中不存在的索引签名。 T 必须可分配给 Record&lt;string, unknown&gt;。但事实并非如此。

    但是,当您将参数键入为 T &amp; Record&lt;string, unknown&gt; 时,您会告诉 Typescript T 可以是任何值,但仅当 T 具有字符串键时才允许使用该参数。如果不是,则类型将解析为never,并且没有任何内容可分配给never

    至少我认为。但老实说,我对 typeinterface 的所有语义并不完全一致。


    或者另一种方法是将所有原始值显式列入黑名单,例如:

    type NotPrimitive<T> =
       T extends string | boolean | number | null | undefined | unknown[]
        ? never
        : T
    
    function doSomething<T>(obj: NotPrimitive<T>) {
      Object.entries(obj).forEach(([key, value]) => {
        console.log(key, value)
      })
    }
    

    Playground

    【讨论】:

    • 谢谢亚历克斯。但是我认为我不能更改参数的类型。我在问题中添加了更多细节。很抱歉,它没有 100% 完成。
    • 您的编辑并不能真正帮助解释事情。您没有将任何内容传递给示例中的函数。但我已经用可能更好的替代方法更新了我的答案。
    • 留在这里是因为我认为它很有用,但@jcalz 的答案要好得多。
    猜你喜欢
    • 2020-10-11
    • 1970-01-01
    • 2019-01-16
    • 2020-05-25
    • 2021-06-29
    • 2021-10-27
    • 2018-03-13
    • 2017-12-14
    • 2021-06-09
    相关资源
    最近更新 更多