跳至主要內容

檢視目錄

關於「慢速類型」

在許多功能中,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 global`、`declare module` 或 `export 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 支援。

如果您使用 JavaScript 程式碼以及 isolatedDeclarations,您的程式碼就已符合 JSR 的「不使用延遲類型」政策。然而,您可能仍然需要針對程式碼進行一些變更以符合 JSR 的其他限制,例如不要使用模組擴充,或全域擴充。

在 GitHub 上編輯此頁面