为何 no-invalid-void-type 允许 void | IType<void>?
void | IType<void> 能通过检查,是因为 no-invalid-void-type 明确允许这种语法形式。规则不会判断 IType<void> 是否只包含“无返回值”语义,也不会因为泛型实参是 void 就认为整个对象不携带数据。官方文档说明,该选项允许的泛型类型也可以作为与 void 联合的成员。
具体检查了什么?
在当前规则实现的 isValidUnionType 中,联合类型的每个直接成员都必须满足以下条件之一:
- 是
void; - 是
never; - 是一个类型引用,且其类型实参列表中直接包含
void。
泛型实参中的 void 还会由 checkGenericTypeArgument 根据配置单独检查。这里检查的是 AST 结构,不会展开接口成员,也不会分析它们的业务含义。规则源码
在题目的配置下:
interface IType<T> {
data: T;
}
// 符合该规则的联合类型例外
type Allowed = () => void | IType<void>;
// undefined 不符合上述成员条件
type Disallowed = () => void | undefined;
// IType<string> 的类型实参列表中没有直接出现 void
type AlsoDisallowed = () => void | IType<string>;
这些注释描述的是该 lint 规则的检查结果,并不意味着任意 SomeType 都能与 void 联合。
为什么要设置这个例外?
一个可以追溯的实际需求是允许同步或异步回调共用同一个签名:
async function run(task: () => void | Promise<void>): Promise<void> {
await task();
}
项目的 issue #1956 提出的就是这种用法:回调有时同步完成,有时返回 Promise<void>,调用方统一等待它完成。
当前实现对泛型的匹配条件较宽,并不局限于 Promise<void>,因此也会放行自定义的 IType<void>。这解释了规则的行为,但不能据此推断所有 T<void> 的语义都相同。
IType 可以携带其他数据,这个疑问成立吗?
成立。例如:
interface IType<T> {
data: T;
message: string;
}
type Callback = () => void | IType<void>;
增加 message 不会改变联合成员的语法形式,所以也不会影响上述检查。规则放行这种写法,并不表示它验证过整个对象是否适合与 void 联合。
实际代码应该怎样写?
先根据返回值的用途决定类型,再调整规则配置。
如果调用方完全忽略返回值,可以使用 () => void。注意,函数类型中的 void 表示调用方不使用返回值,不能保证被赋值的函数在运行时一定返回 undefined。TypeScript 手册
如果调用方需要区分“没有结果”和“返回了对象”,可以明确使用 undefined:
type GetResult = () => IType<void> | undefined;
这会改变类型契约,不能视为对原类型的无条件等价替换。
如果项目只希望允许少数泛型接收 void,可以把配置收紧为名称允许列表,例如:
{
"@typescript-eslint/no-invalid-void-type": [
"error",
{
"allowInGenericTypeArguments": ["Promise"]
}
]
}
这样也会禁止 IType<void> 本身,影响范围不限于联合返回类型。该选项没有单独提供开关,让你既允许所有泛型实参中的 void,又禁止它们与顶层 void 联合。选项说明
原问题没有提供插件版本。以上实现细节以所链接的源码为依据,排查具体项目时应对照已安装版本。
备注:内容仅供参考。
