跳至主要内容

查看目录

关于“慢类型”

在许多功能中,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 代码施加的所有限制

  1. 所有导出的函数、类和变量必须具有显式类型。例如,函数应该具有显式返回类型,类应该为其属性具有显式类型,常量应该具有显式类型注释。

  2. 模块增强和全局增强不能使用。这意味着包不能使用 declare globaldeclare moduleexport as namespace 来增强全局范围或其他模块。

  3. 不允许使用 CommonJS 功能。这意味着包不能使用 `export =` 或 `import foo = require("foo")`。

  4. 导出函数、类、变量和类型中的所有类型必须简单推断或显式声明。如果表达式过于复杂而无法推断,则应将其类型显式分配给中间类型。

  5. 不支持导出中的解构。不要使用解构,而是单独导出每个符号。

  6. 类型不能引用类的私有字段。

显式类型

从包中导出的所有符号必须显式指定类型。例如,函数应具有显式的返回类型

- 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 编译器对类型执行类型扩展或缩小操作(例如包含不同形状的对象文字的数组),则也不可能。

简单推断可以在两个位置进行

  1. 如果箭头函数的函数体是一个简单的表达式而不是一个代码块,则该箭头函数的返回类型。
  2. 用简单表达式初始化的变量(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`.
}

这种推断只能对少数几个简单的表达式执行。这些是

  1. 数字字面量。
    5;
    1.5;
  2. 字符串字面量。
    "hello";
    // Template strings are not supported.
  3. 布尔字面量。
    true;
    false;
  4. nullundefined
    null;
    undefined;
  5. BigInt 字面量。
    5n;
  6. as T 断言
    foo() as MyType; // The type is `MyType`.
  7. Symbol()Symbol.for() 表达式。
    Symbol("foo");
    Symbol.for("foo");
  8. 正则表达式。
    /foo/;
  9. 具有简单表达式作为属性的数组字面量(不包括对象字面量)。
    [1, 2, 3];
  10. 具有简单表达式作为属性的对象字面量。
    { foo: 5, bar: "hello" };
  11. 完全注释的函数或箭头函数(即所有参数和返回类型都已注释或简单推断)。
    const x = (a: number, b: number): number => a + b;

忽略慢速类型

由于它们影响 JSR 是否能够理解代码的本质,因此您不能选择性地忽略慢速类型的单个诊断。慢速类型诊断只能针对整个包进行忽略。这样做会导致包的文档和类型声明不完整,以及包使用者类型检查速度变慢。

要忽略包的慢速类型诊断,请将 --allow-slow-types 标志添加到 jsr publishdeno 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 之类的忽略注释来忽略慢速类型诊断。

与 TypeScript isolatedDeclarations 的交互

从 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 的其他限制,例如不使用模块增强或全局增强。

在 GitHub 上编辑此页面