关于“慢类型”
在许多功能中,JSR 会分析源代码,特别是源代码中的 TypeScript 类型。这样做是为了生成文档,为 npm 兼容性层生成类型声明,以及加快使用 JSR 中包的 Deno 项目的类型检查速度。
为了使这些功能正常工作,TypeScript 源代码不得导出任何函数、类、接口或变量,或类型别名,这些函数、类、接口或变量本身或引用“慢类型”。“慢类型”是指未明确写入的类型,或复杂到需要大量推断才能理解的类型。
这种推断需要对 TypeScript 源代码进行完整的类型检查,这在 JSR 中不可行,因为这需要运行 TypeScript 编译器。它不可行是因为 tsc
不会随着时间的推移产生稳定的类型信息,并且在定期运行注册表中的每个包时速度太慢(在此问题中了解更多信息)。因此,这些类型的类型在公共 API 中不受支持。
⚠️ 如果 JSR 在包中发现“慢类型”,某些功能将无法正常工作或质量下降。这些是
- 对包的使用者进行类型检查将变慢。对于大多数包来说,减速至少是 1.5-2 倍。它可能更高。
- 该包将无法为 npm 兼容层生成类型声明,或者“慢类型”将在生成的类型声明中被省略或替换为
any
。- 该包将无法为该包生成文档,或者“慢类型”将在生成的文档中被省略或缺少详细信息。
什么是慢类型?
“慢类型”发生在从包中导出函数、类、const 声明或 let 声明时,并且其类型没有显式写入或类型比可以简单推断的类型更复杂。
一些“慢类型”的示例
// This function is problematic because the return type is not explicitly
// written, so it would have to be inferred from the function body.
export function foo() {
return Math.random().toString();
}
const foo = "foo";
const bar = "bar";
export class MyClass {
// This property is problematic because the type is not explicitly written, so
// it would have to be inferred from the initializer.
prop = foo + " " + bar;
}
对包内部(即未从包清单的exports
中的文件中导出)的慢类型对 JSR 和包的使用者来说没有问题。因此,如果您有一个未导出的慢类型,您可以将其保留原样
export add(a: number, b: number): number {
return addInternal(a, b);
}
// It is ok to not explicitly write the return type of this function, because it
// is not exported (it is internal to the package).
function addInternal(a: number, b: number) {
return a + b;
}
TypeScript 限制
本节列出了“无慢类型”策略对 TypeScript 代码施加的所有限制
所有导出的函数、类和变量必须具有显式类型。例如,函数应该具有显式返回类型,类应该为其属性具有显式类型,常量应该具有显式类型注释。
模块增强和全局增强不能使用。这意味着包不能使用
declare global
、declare module
或export as namespace
来增强全局范围或其他模块。不允许使用 CommonJS 功能。这意味着包不能使用 `export =` 或 `import foo = require("foo")`。
导出函数、类、变量和类型中的所有类型必须简单推断或显式声明。如果表达式过于复杂而无法推断,则应将其类型显式分配给中间类型。
不支持导出中的解构。不要使用解构,而是单独导出每个符号。
类型不能引用类的私有字段。
显式类型
从包中导出的所有符号必须显式指定类型。例如,函数应具有显式的返回类型
- export function add(a: number, b: number) {
+ export function add(a: number, b: number): number {
return a + b;
}
类应为其属性显式声明类型
export class Person {
- name;
- age;
+ name: string;
+ age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
常量应具有显式类型注释
- export const GLOBAL_ID = crypto.randomUUID();
+ export const GLOBAL_ID: string = crypto.randomUUID();
全局增强
不允许使用模块增强和全局增强。这意味着包不能使用 `declare global` 来引入新的全局变量,也不能使用 `declare module` 来增强其他模块。
以下是一些不支持的代码示例
declare global {
const globalVariable: string;
}
declare module "some-module" {
const someModuleVariable: string;
}
CommonJS 功能
不允许使用 CommonJS 功能。这意味着包不能使用 `export =` 或 `import foo = require("foo")`。
请使用 ESM 语法代替
- export = 5;
+ export default 5;
- import foo = require("foo");
+ import foo from "foo";
类型必须是简单推断或显式
导出函数、类、变量和类型中的所有类型必须是 简单推断 或显式。如果表达式过于复杂而无法推断,则应将其类型显式分配给中间类型。
例如,在以下情况下,默认导出的类型过于复杂而无法推断,因此必须使用中间类型显式声明
class Class {}
- export default {
- test: new Class(),
- };
+ const obj: { test: Class } = {
+ test: new Class(),
+ };
+
+ export default obj;
或使用 as
断言
class Class {}
export default {
test: new Class(),
- };
+ } as { test: Class };
对于超类表达式,请评估表达式并将其分配给中间类型
interface ISuperClass {}
function getSuperClass() {
return class SuperClass implements ISuperClass {};
}
- export class MyClass extends getSuperClass() {}
+ const SuperClass: ISuperClass = getSuperClass();
+ export class MyClass extends SuperClass {}
导出中不允许解构
导出中的解构不受支持。不要解构,而是单独导出每个符号
- export const { foo, bar } = { foo: 5, bar: "world" };
+ const obj = { foo: 5, bar: "world" };
+ export const foo: number = obj.foo;
+ export const bar: string = obj.bar;
类型不能引用类的私有字段
类型在推断期间不能引用类的私有字段。
在此示例中,公共字段引用了私有字段,这是不允许的。
export class MyClass {
- prop!: typeof MyClass.prototype.myPrivateMember;
- private myPrivateMember!: string;
+ prop!: MyPrivateMember;
+ private myPrivateMember!: MyPrivateMember;
}
+ type MyPrivateMember = string;
简单推断
在某些情况下,JSR 可以推断类型,而无需您显式指定。这些情况称为“简单推断”。可以简单推断的类型不被视为“慢类型”。
通常,如果符号不引用其他符号,则可以进行简单推断。如果 TypeScript 编译器对类型执行类型扩展或缩小操作(例如包含不同形状的对象文字的数组),则也不可能。
简单推断可以在两个位置进行
- 如果箭头函数的函数体是一个简单的表达式而不是一个代码块,则该箭头函数的返回类型。
- 用简单表达式初始化的变量(const 或 let 声明)或属性的类型。
export const foo = 5; // The type of `foo` is `number`.
export const bar = () => 5; // The type of `bar` is `() => number`.
class MyClass {
prop = 5; // The type of `prop` is `number`.
}
这种推断只能对少数几个简单的表达式执行。这些是
- 数字字面量。
5; 1.5;
- 字符串字面量。
"hello"; // Template strings are not supported.
- 布尔字面量。
true; false;
null
和undefined
。null; undefined;
- BigInt 字面量。
5n;
as T
断言foo() as MyType; // The type is `MyType`.
Symbol()
和Symbol.for()
表达式。Symbol("foo"); Symbol.for("foo");
- 正则表达式。
/foo/;
- 具有简单表达式作为属性的数组字面量(不包括对象字面量)。
[1, 2, 3];
- 具有简单表达式作为属性的对象字面量。
{ foo: 5, bar: "hello" };
- 完全注释的函数或箭头函数(即所有参数和返回类型都已注释或简单推断)。
const x = (a: number, b: number): number => a + b;
忽略慢速类型
由于它们影响 JSR 是否能够理解代码的本质,因此您不能选择性地忽略慢速类型的单个诊断。慢速类型诊断只能针对整个包进行忽略。这样做会导致包的文档和类型声明不完整,以及包使用者类型检查速度变慢。
要忽略包的慢速类型诊断,请将 --allow-slow-types
标志添加到 jsr publish
或 deno publish
中。
使用 Deno 时,可以通过添加 no-slow-types
规则的排除项来阻止慢速类型诊断在 deno lint
中显示。这可以通过在运行 deno lint
时指定 --rules-exclude=no-slow-types
来完成,或者通过将以下内容添加到您的 deno.json
配置文件中来完成
{
"lint": {
"rules": {
"exclude": ["no-slow-types"]
}
}
}
请注意,由于无法单独忽略慢速类型诊断,因此无法使用诸如 // deno-lint-ignore no-slow-types
之类的忽略注释来忽略慢速类型诊断。
isolatedDeclarations
的交互
与 TypeScript 从 TypeScript 5.5 开始,TypeScript 引入了一个名为 isolatedDeclarations
的编译器选项。启用此选项后,将禁止编写需要类型推断才能发出声明文件的类型。这与 JSR 的“无慢速类型”策略非常相似。
例如,就像 JSR 一样,启用了 `isolatedDeclarations` 的 TypeScript 不会允许没有显式返回类型的函数声明。
// This is not allowed with `isolatedDeclarations`.
export function foo() {
return Math.random().toString();
}
但是,两者之间有一些区别。
- 隔离声明要求从模块导出的所有符号都遵循“无推断”规则。JSR 仅要求实际属于包的公共 API 部分的符号遵循“无推断”规则。
- 隔离声明有时比 JSR 更严格。例如,隔离声明不支持将空函数体的返回类型推断为 `void`,而 JSR 支持。
如果您使用启用了 `isolatedDeclarations` 的 TypeScript,您的代码已经符合 JSR 的“无慢类型”策略。但是,您可能仍然需要对代码进行一些更改以符合 JSR 的其他限制,例如不使用模块增强或全局增强。