關於「慢速類型」
在許多功能中,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 支援。
如果您使用 JavaScript 程式碼以及 isolatedDeclarations
,您的程式碼就已符合 JSR 的「不使用延遲類型」政策。然而,您可能仍然需要針對程式碼進行一些變更以符合 JSR 的其他限制,例如不要使用模組擴充,或全域擴充。