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



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

下面是我正在使用的函数。然而,这个函数是在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;
}

对于以下示例,它应该抛出一个错误(基本上它不应该接受任何基本类型,如string, number, boolean, null, undefined,…):

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扩展Record<string,>

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

如果我把unknown改为any,它就会起作用,但我认为在Typescript中应该避免使用any。

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

正确的做法是将类型参数T约束为object类型,这特别意味着"非基本类型":

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的ban-types规则与默认配置,它抱怨object的警告形式:

避免object类型,因为它目前很难使用,因为不能断言键存在。看到微软/打印稿# 21732 .

该规则在某些情况下可能是善意和有用的,object确实有一些缺点,但正如您所看到的,Record<string, unknown>并不总是一个改进。某物"难以使用"。大概并不意味着应该完全禁止它,而支持其他东西,特别是如果其他东西对用例不起作用。用刀开罐头很难,你应该用开罐器来代替,但这并不意味着你应该用开罐器来切面包。不同的类型有不同的用例。上面代码中的T extends object似乎是100%适合这项工作的工具。

我可能对这个问题有点过于情绪化,因为我是提交microsoft/TypeScript#21732的人,我希望看到key in obj作为obj的类型保护,断言属性存在。但是这个问题绝对不意味着object是无用的,看到所有这些GitHub问题在其他项目链接到这是他们小心翼翼地从他们的代码中删除object的原因有点筋疲力尽。

哦!

Playground链接到代码

我在给出这个答案的同时也要说明,遗憾的是,我并不是百分之百确定为什么这是有效的。

如果你离开通用无约束,使参数类型:

T & Record<string, unknown>

那么它就像你期望的那样工作了。

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

游乐场


假说:

当你推断一个泛型形参时,它必须完全满足这个约束。在这种情况下,Record暗示了interface中不存在的索引签名。T必须可分配给Record<string, unknown>。但它不是。

然而,当你输入参数T & Record<string, unknown>时,你告诉TypescriptT可以是任何东西,但只有当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)
})
}

游乐场

最新更新