如何将方法的泛型类型限制为打字稿中的对象?

Posted

技术标签:

【中文标题】如何将方法的泛型类型限制为打字稿中的对象?【英文标题】: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

【问题讨论】:

【参考方案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 的答案要好得多。

以上是关于如何将方法的泛型类型限制为打字稿中的对象?的主要内容,如果未能解决你的问题,请参考以下文章

泛型的泛型的好处

无法将X类型转换为T.泛型类中的泛型委托

当类的泛型相关时,如何在两个泛型类之间创建类似子类型的关系呢

Java 泛型泛型简介 ( 泛型类 | 泛型方法 | 静态方法的泛型 | 泛型类与泛型方法完整示例 )

怎么取出类的泛型类型

Java中的泛型的问题?