问题分析
用typescript写代码的你,这个错误不陌生吧?
触发这条报错很简单,如果你写出了这样的代码:
type Empty = {}
那么,当你使用eslint检查你的typescript代码时,如果你没有对eslint进行特殊配置,@typescript-eslint/ban-types这条规则就会报上面的错误。
当然,如果你使用几乎等同于{}的Object,同样会触发报错:
鉴于{}和Object一样,以下就省略Object了。
之所以eslint会有这么条规则,主要是因为{}的真正语义,和大多数人的直觉不太一样:
object指的是non-primitive,可以理解为“随便一个对象”;{}指的是non-nullish,可以理解为“随便一个值”,可以是1、"abc"、symbol("")等原始数据,但是不能是null和undefined;
比如你可以这么写:
type Empty = {} // or type Empty = Object
type T = {
foobar: Empty
}
const t: T = { foobar: 1 }
tsc(4.5.4)针对上面的代码是不会报错的,原因在于typescript支持javascript的数据自动装箱机制,导致要求传入{}的位置,也允许传入一个原始数据。
这有点反直觉,于是eslint干脆不允许你使用{}类型了。
关于这条规则的具体讨论可以看这条issue:github.com/typescript-…
解决方法
1.不建议屏蔽该报错
除非实在没办法,否则不要修改eslintrc,影响面太大。也不建议使用//@ts-ignore,因为{}实际上几乎能匹配任意类型,所以使用它来做类型约束是不安全的。
2.有时候<T extends {}>是<T extends object>的错误写法
有时候,使用{}的人其实是想要表达的就是object,但是代码写错了。尤其是那些写<T extends {}>的人,他们往往要表达的是“T是任意对象类型”,而不是“T是任意non-nullish类型”。
比如如果你原来这么写:
export type Constructor<T extends {}> = new (...args: any[]) => T
那你可以改成:
export type Constructor<T extends object> = new (...args: any[]) => T
语义略有点问题,但问题不大。
3.有时候{}是Record<keyof any, unknown>的错误写法
eslint更加精致的辨析了{}的误用情况,给出了三条建议(如下图):
这里面比较make sense的是第一个建议,Record<keyof any, unknown>表达出了“随便一个对象”的语义。但如果是跟extends连用,用来校验类型参数的话,是不合适的,比如下面这种情况:
原来的错误代码是这样:
type SomeTypeTrick<C extends {}> = C //某种类型体操
class SomeClass {} // 某个具体类
type R = SomeTypeTrick<typeof SomeClass>
如果你改成这样,tsc就会报错:
type SomeTypeTrick<C extends Record<keyof any, unknown>> = C
class SomeClass {}
type R = SomeTypeTrick<typeof SomeClass> // Error: Index signature for type 'string' is missing in type 'SomeClass'
之所以报错,是因为在官方的lib.es5.d.ts中Record长这样:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
所以Record<keyof any, unknown>>实际上定义了无数个成员字段,只不过都是unknown而已,而SomeClass显然没有那么多成员字段。
4.如果你非得用{}
可能在某些情况下,你真的就是想用{}表达non-nullish语义,那么推荐一个“奇技淫巧”给你:
type NonNullishValue = Omit<Record<keyof any, never>, keyof any>
上面的代码中,NonNullishValue就是{},但是至少到@typescript-eslint/eslint-plugin@5.9.0为止,还不会对这种写法报错,祝你好运。