如何将方法的泛型类型限制为打字稿中的对象?
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')
我已经尝试过像这样更改方法:
函数 doSomething
它适用于大多数示例(意味着它会引发错误),但在使用接口时它不起作用(抛出索引签名缺失)。将现有接口更改为类型不是解决方案,我认为如果示例函数是库的一部分,我作为库的用户希望同时拥有接口和类型这两个选项。
但是,如果我将 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<string, unknown>
并不总是一种改进。某些东西“难以使用”大概并不意味着应该完全禁止它以支持其他东西,特别是如果其他东西不适用于该用例。用刀打开罐头很难,你应该改用开罐器,但这并不意味着你应该尝试用开罐器切面包。不同的类型有不同的用例。并且上面代码中的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<string, unknown>
。但事实并非如此。
但是,当您将参数键入为 T & Record<string, unknown>
时,您会告诉 Typescript T
可以是任何值,但仅当 T
具有字符串键时才允许使用该参数。如果不是,则类型将解析为never
,并且没有任何内容可分配给never
。
至少我认为。但老实说,我对 type
与 interface
的所有语义并不完全一致。
或者另一种方法是将所有原始值显式列入黑名单,例如:
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 的答案要好得多。以上是关于如何将方法的泛型类型限制为打字稿中的对象?的主要内容,如果未能解决你的问题,请参考以下文章
当类的泛型相关时,如何在两个泛型类之间创建类似子类型的关系呢