typescript-eslintban-types规则移除与迁移指南:no-restricted-types 等四个替代规则的完整解析
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
本文是一份面向 ESLint + TypeScript 用户的迁移指南,核心围绕 typescript-eslint 中经典的ban-types规则在 v8 中被拆分、弃用并最终移除的完整过程展开。你将了解到ban-types为何被重构、它被拆分为哪四个新规则(no-restricted-types、no-empty-object-type、no-unsafe-function-type、no-wrapper-object-types),每个新规则的具体配置方法、源码实现原理、默认启用情况,以及如何把旧的ban-types配置平滑迁移到新规则,让升级到 typescript-eslint v8 不再困惑。
一、背景:ban-types规则为何被拆掉
在 typescript-eslint 的早期版本中,@typescript-eslint/ban-types是最具代表性的规则之一,它同时承担了三个职责:
- 封禁不安全的"空对象"类型
{}; - 封禁危险或具有误导性的内置类型,例如
Function、Number等; - 允许用户自定义额外的封禁类型名单。
这三个目标都是很好的 lint 方向,但把三者揉进同一条规则带来了明显的设计问题(详见官方博客 Revamping theban-typesrule):
- 难以按需配置:由于同时覆盖三个领域,用户无法只针对自己项目需要的部分进行轻量配置;
- 对
{}过于严苛:{}的语义在何时该用、何时不该用上有很强的"灰度",这种微妙之处无法用简单的规则配置格式表达,导致大量误报和困惑; - 默认封禁类型修复能力有限:对默认封禁的内置类型,能提供的自动修复和边界情况处理非常有限。
这一问题的根源可以追溯到 TSLint。最早的"封禁类型"规则来自 TSLint 的ban-types,它默认不封禁任何类型,只封禁用户在配置里显式声明的类型。当该规则移植到@typescript-eslint/ban-types时,改为默认额外封禁一批已知危险的内置类型,这样plugin:typescript-eslint/recommended预设配置就能默认开启这些检查。副作用是,像object与{}这种需要精细区分的类型,也被塞进了和Function一样的简单配置通道里。
因此,typescript-eslint v8 将旧的ban-types规则拆分为多个更聚焦的规则,ban-types本身在 v8 中被移除。官方文档明确保留ban-types页面(ban-types.md)的唯一目的,就是为了把用户引导到替代规则上——由于该页面不参与正常导航,只能通过搜索框进入。
二、迁移总览:一个规则变成四个
ban-types的功能被拆解为四份,分别对应四个新规则:
原ban-types职责 | 新规则 | 定位 | 是否默认开启 |
|---|---|---|---|
| 封禁用户自定义的任意类型名 | no-restricted-types | 纯用户配置,默认无任何封禁 | 否(仅all预设) |
封禁令人困惑的内置{}空对象类型 | no-empty-object-type | 默认封禁,提供建议修复 | 是(recommended) |
封禁不安全的Function内置类型 | no-unsafe-function-type | 默认封禁,无配置项 | 是(recommended) |
封禁Object及Number等内置包装类型 | no-wrapper-object-types | 默认封禁,提供自动修复 | 是(recommended) |
其中no-restricted-types的行为与 ESLint 核心规则中的no-restricted-globals与 flat 版 recommended 配置 中,no-empty-object-type、no-unsafe-function-type、no-wrapper-object-types均以'error'开启,而no-restricted-types只出现在all预设(eslintrc/all.ts、flat/all.ts)中,且同样为'error'。
三、no-restricted-types:用户自定义封禁类型名单
no-restricted-types是四个新规则中与旧ban-types用户自定义能力最接近的规则,用于封禁特定的类型注解。典型场景是项目正在从某个类型迁移到另一个类型,希望禁止对旧类型的引用。需要注意的是,该规则只封禁类型层面的引用,并不封禁对应的运行时对象。
3.1 可封禁的类型形式
被封禁的类型既可以是类型名字面量(如OldType),也可以是带泛型参数实例化的类型名(如OldType<MyArgument>)。配置值支持三种形态:
- 字符串:作为命中该类型时展示的错误消息;
- 对象,包含以下可选属性:
message: string:类型被命中时展示的消息;fixWith?: string:运行自动修复时用来替换封禁类型的字符串,省略则不做修复;suggest?: string[]:提供给用户的建议替换列表(非自动修复,由编辑器提示用户选择)。
- 布尔值
true:使用默认消息封禁该类型(源码 schema 中允许true,见 no-restricted-types.ts)。
3.2 完整配置示例
{ "@typescript-eslint/no-restricted-types": [ "error", { "types": { // 自定义消息,解释为什么不该用它 "OldType": "Don't use OldType because it is unsafe", // 自定义消息 + 告诉插件如何自动修复 "OldAPI": { "message": "Use NewAPI instead", "fixWith": "NewAPI", }, // 自定义消息 + 提供建议修复(由用户在编辑器中选择) "SoonToBeOldAPI": { "message": "Use NewAPI instead", "suggest": ["NewAPIOne", "NewAPITwo"], }, }, }, ], }该规则的源码还会对配置中的类型名做去空格处理(removeSpaces移除所有空白字符),并对代码中实际出现的类型名做同样的归一化后再匹配,这意味着OldAPI与代码中写作OldAPI <string>这类含空格的写法也能被正确识别(见 no-restricted-types.ts)。
3.3 源码层面的检查范围
从 no-restricted-types.ts 的监听器可以看出,规则覆盖了以下 AST 节点:
TSTypeReference:普通类型引用(含带类型实参的写法);TSClassImplements/TSInterfaceHeritage:implements与extends子句;TSTypeLiteral与TSTupleType:空对象字面量类型{}与空元组[];- 内置类型关键字(
bigint、boolean、number、string、symbol、unknown、void等,见 TYPE_KEYWORDS 映射):仅当配置中显式封禁了对应关键字时才挂载监听器。
对命中的节点,规则会报告Don't use \{{name}}` as a type.{{customMessage}};配置了fixWith时提供自动修复(fixer.replaceText将节点整体替换),配置了suggest时提供Replace `{{name}}` with `{{replacement}}`的建议修复。注意自动修复是可以被--fix直接应用的,因此官方在 schema 描述中特别提醒fixWith` 要谨慎使用。
3.4 何时不该用
如果你没有"封禁特定类型"的需求,就不需要这条规则。
四、no-empty-object-type:{}空对象类型的迷惑与替代
{}("空对象"类型)是 TypeScript 结构化类型体系里最容易让新手困惑的写法。{}表示任意"非 nullish"值,包括字面量0和"":
let anyNonNullishValue: {} = 'Intentionally allowed by TypeScript.';也就是说,{}真正的含义是"任意被定义的值"——包括数组、类实例、函数以及string、symbol等原始类型。因此,开发者写{}时通常想表达的是:
object:表示任意对象值;unknown:表示包括null和undefined在内的任意值。
为避免这种语义混淆,no-empty-object-type默认封禁{}类型的使用,涵盖无字段的接口声明和空的对象类型别名。
4.1 正确与错误示例
❌ 错误写法:
let anyObject: {}; let anyValue: {}; interface AnyObjectA {} interface AnyValueA {} type AnyObjectB = {}; type AnyValueB = {};✅ 正确写法:
let anyObject: object; let anyValue: unknown; type AnyObjectA = object; type AnyValueA = unknown; type AnyObjectB = object; type AnyValueB = unknown; let objectWith: { property: boolean }; interface InterfaceWith { property: boolean; } type TypeWith = { property: boolean };4.2 规则默认放行的两种情况
该规则刻意不报告以下两种场景(这正是旧ban-types配置格式无法表达、而新规则内建豁免的边界情况):
- 作为交叉类型成员的
{}(例如 TypeScript 内置的type NonNullable<T> = T & {}),它在类型系统运算中是有用且合理的; - 继承自多个其他接口的接口(
interface A extends B, C {}这类没有新增字段的空接口)。
4.3 选项详解
该规则默认同时检查空接口与空对象类型(allowInterfaces、allowObjectTypes默认均为'never',见 no-empty-object-type.ts)。
allowInterfaces可取值:
'always':始终允许无字段的接口;'never'(默认):从不允许无字段的接口;'with-single-extends':允许仅继承单个基接口的空接口。
{ allowInterfaces: 'with-single-extends' }下的正确代码:
interface Base { value: boolean; } interface Derived extends Base {}allowObjectTypes可取值:
'always':始终允许无字段的对象字面量类型;'never'(默认):从不允许。
allowWithName:一个字符串形式的正则表达式,用于按名字放行空接口/空对象类型别名。如果你的既有代码风格习惯用{}而非object声明空类型,这个选项会很有用。例如{ allowWithName: 'Props$' }:
❌ 仍会报错:
interface InterfaceValue {} type TypeValue = {};✅ 被放行:
interface InterfaceProps {} type TypeProps = {};从源码看,allowWithName会编译为带u标志的正则(new RegExp(allowWithName, 'u')),同时作用于接口名(TSInterfaceDeclaration)和类型别名(TSTypeAliasDeclaration包装的空TSTypeLiteral)。此外,源码对空接口还有一个额外豁免:如果该名字与类声明或另一个接口声明合并(mergedWithOtherDeclaration,即声明合并场景)或为默认导出,则不提供自动替换建议,避免破坏既有代码结构(见 no-empty-object-type.ts)。
该规则属于suggestion类型,会针对空接口/空对象分别给出替换为object或unknown的建议修复:对空对象直接fixer.replaceText替换节点;对空接口则将其改写为type X = object | unknown形式的类型别名(源码中的replaceEmptyInterface修复逻辑,no-empty-object-type.ts)。
4.4 何时不该用
如果你的代码经常需要表示"任意非 nullish 值",或者大量使用条件类型、映射类型等类型运算,该规则可能不适合你的项目,可以考虑关闭。若确实有 API 需要接收{},官方建议通过配置规则选项、使用 ESLint 禁用注释或直接在 ESLint 配置中关闭规则来处理。另外,与该规则配套的还有no-generated-empty-object-type,它负责报告经类型运算解析后得到{}的情况,而本规则只处理手写的{}。
五、no-unsafe-function-type:封禁危险的Function类型
TypeScript 内置的Function类型允许以任意数量的参数调用,且返回类型是any。Function还允许恰好具备Function类全部属性的类或普通对象被赋值。这意味着Function基本抹掉了所有函数签名的类型安全,应尽量用函数类型语法明确参数与返回值类型。
5.1 正确与错误示例
❌ 错误写法:
let noParametersOrReturn: Function; noParametersOrReturn = () => {}; let stringToNumber: Function; stringToNumber = (text: string) => text.length; let identity: Function; identity = value => value;✅ 正确写法(可参考的"兜底"函数类型):
let noParametersOrReturn: () => void; noParametersOrReturn = () => {}; let stringToNumber: (text: string) => number; stringToNumber = text => text.length; let identity: <T>(value: T) => T; identity = value => value;官方文档还给出了两种常见的"兜底/全捕获"函数类型供参考:
() => void:无参数、返回值被忽略的函数;(...args: never) => unknown:函数的"top type",可赋值给任意函数类型,但本身不可被调用。
5.2 源码实现要点
该规则是一个problem型、无任何配置项的规则。在 no-unsafe-function-type.ts 中,规则通过isReferenceToGlobalFunction('Function', node, context.sourceCode)判断标识符是否真正指向全局内置的Function类型——这意味着如果你在作用域内自行声明了同名类型或变量,规则不会误报。检查覆盖TSClassImplements、TSInterfaceHeritage与TSTypeReference三类节点。报告消息为:The \Function` type accepts any function-like value. Prefer explicitly defining any function parameters and return type.`
5.3 何时不该用
如果项目还处于 TypeScript 迁移初期,短期内难以把全部不安全的Function类型替换为精确的函数类型,可以考虑在个别场景使用 ESLint 禁用注释,而不是整体关闭规则。
六、no-wrapper-object-types:封禁大小写混淆的包装对象类型
TypeScript 定义了若干组"看起来很像、实则含义完全不同"的类型对:boolean/Boolean、number/Number、string/String、bigint/BigInt、symbol/Symbol、object/Object。一般来说只应使用小写变体,本规则强制这一点。
6.1 为什么必须用小写
JavaScript 在运行时只有 8 种数据类型,对应 TypeScript 的小写类型:undefined、null、boolean、number、string、bigint、symbol、object。而大写类型是结构化类型,描述的是各数据类型的 JavaScript"包装"对象(如Boolean、Number)。由于结构化类型的"形状相同"怪癖,对应原始类型也能赋值给这些大写类型(let myObject: Object = 'allowed by TypeScript';能通过编译)。在运行时,包装对象与原始值的行为差异巨大:
- 相等性:原始值按值比较(
"str" === "str"),包装对象按引用比较(new String("str") !== new String("str")); - 真值性:原始值有大家依赖的真假值规则,而所有对象恒为真——即使
new Boolean(false)也为真; - 运算限制:TypeScript 只允许对数值原始类型进行算术运算(如
x - y),不允许对对象进行。
因此用number而非Number才能更准确地描述代码。这是普遍的最佳实践:直接使用0这样的原始值,而非"长得像原始值"的对象new Number(0)。
6.2 正确与错误示例
❌ 错误写法:
let myBigInt: BigInt; let myBoolean: Boolean; let myNumber: Number; let myString: String; let mySymbol: Symbol; let myObject: Object = 'allowed by TypeScript';✅ 正确写法:
let myBigint: bigint; let myBoolean: boolean; let myNumber: number; let myString: string; let mySymbol: symbol; let myObject: object = "Type 'string' is not assignable to type 'object'.";6.3 源码实现要点
该规则是problem型规则,fixable: 'code'。在 no-wrapper-object-types.ts 中,被检查的类名集合固定为BigInt、Boolean、Number、Object、String、Symbol六个;与no-unsafe-function-type相同,也通过isReferenceToGlobalFunction确认是全局内置引用后才报告。规则报告Prefer using the primitive \{{preferred}}` as a type name, rather than the upper-cased `{{typeName}}`.,其中preferred` 即小写化后的类型名。
自动修复逻辑(no-wrapper-object-types.ts)有一个细节值得注意:对于TSTypeReference(普通类型注解位置)提供自动修复(fixer.replaceText替换为小写),而对于TSClassImplements与TSInterfaceHeritage(implements/extends子句)则不提供修复——因为在继承/实现位置直接改成小写类型会改变语义。
6.4 何时不该用
如果你的项目极少数情况下确实需要处理原始类型的类等价物,可以仅在那些具体位置使用禁用注释,而不是整体关闭规则。
七、升级迁移路径:从ban-types到新规则
7.1 直接迁移策略
由于三个默认封禁规则(no-empty-object-type、no-unsafe-function-type、no-wrapper-object-types)已包含在recommended预设中,使用plugin:typescript-eslint/recommended(或 flat config 的tseslint.configs.recommended)的项目升级到 v8 后会自动获得原ban-types默认封禁行为,无需额外配置。
对于在旧ban-types中自定义封禁的类型名单,需要手动迁移到no-restricted-types的types配置中。对照关系如下:
旧ban-types写法 | 新no-restricted-types写法 |
|---|---|
"types": { "OldType": "自定义消息" } | "types": { "OldType": "自定义消息" } |
| 字符串消息 | 字符串消息(不变) |
带message对象 | 带message对象(不变) |
fixWith自动替换 | fixWith自动替换(新增于旧版之上) |
| — | suggest建议列表(新规则独有能力) |
从配置语义上看,旧规则的用户自定义部分基本是"平移"到新规则即可,而新规则额外带来了suggest(编辑器内建议修复)能力——适用于不确定修复是否可靠、不想让--fix自动应用的场景。官方博客在 Revamping theban-typesrule 中也给出了一个"封禁旧 API 并提供两个建议"的典型配置:
{ "@typescript-eslint/no-restricted-types": [ "error", { "types": { "DeprecatedOldAPI": { "message": "Use either NewAPIOne or NewAPITwo instead", "suggest": ["NewAPIOne", "NewAPITwo"], }, }, }, ], }7.2 依赖项检查
由于ban-types在 typescript-eslint v8 中被移除,升级前请检查项目中是否还直接引用了@typescript-eslint/ban-types规则名,并将其替换为上表中的四个新规则。官方在 v8 相关发布说明(Announcing typescript-eslint v8 Beta、Announcing typescript-eslint v8)中列出了全部破坏性变更,建议升级前通读。若仍需历史背景与拆分细节,可阅读仓库内保留的 ban-types.md 与 Revamping theban-typesrule。
八、四个新规则的测试覆盖
四个新规则在仓库中均有完整的测试套件,可当作理解规则行为的补充材料:
- no-restricted-types.test.ts:覆盖自定义消息、
fixWith自动修复、suggest建议、泛型类型名匹配、关键字类型封禁等场景; - no-empty-object-type.test.ts:覆盖
allowInterfaces三种取值、allowObjectTypes、allowWithName正则、声明合并豁免等场景; - no-unsafe-function-type.test.ts:覆盖全局
Function识别与局部同名遮蔽等场景; - no-wrapper-object-types.test.ts:覆盖六个大写包装类型、自动修复、
implements子句不修复等场景。
九、总结与决策建议
ban-types从"一个规则管三件事"重构为"四条单一职责规则",是 typescript-eslint v8 中一次典型的"按关注点拆分"演进。迁移时只需记住一条主线:默认封禁交给推荐配置,自定义封禁交给no-restricted-types。
- 使用
recommended(或strict)预设的用户,升级 v8 后{}、Function、包装类型三类默认封禁自动生效; - 曾自定义
ban-types.types的用户,把配置平移进no-restricted-types,并可按需利用新增的suggest建议修复; - 需要精细控制空接口/空对象类型的用户,通过
no-empty-object-type的allowInterfaces、allowObjectTypes、allowWithName三个选项即可覆盖绝大多数边界场景,这些精细化选项正是旧ban-types无法提供的。
通过本文的配置示例与源码分析,你可以在升级到 typescript-eslint v8 后,快速完成ban-types相关配置的迁移,并借助新规则更精细的能力治理代码中的危险类型用法。
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考