建構 Angular library 注意 ES2022 與 useDefineForClassFields

Eric Li
hefemk
Published in
6 min readJul 4, 2023

最近某個 Library 在升級至 Angular 15 後,開發階段正常,但建置(Build)後就會特定的變數初始化失敗,且連 constructor 都未被執行。

分析後其實很單純,倒是一連串誤會形成迷霧,過程中的發現還是有些說明與提醒的意義,所以整理了一下。

範例

為了方便說明,以下的簡單範例以最單純的方式呈現,模擬一個 Component 注入一個 Service

class Service {
public getName(): string {
return 'User';
}
}

class Component {
private greeting = 'Hi,' + this.service.getName();
constructor(
private service: Service,
) {}
}

敏感的讀者,也許已經覺得 greeting 這樣的宣告與賦值有些怪味道,確實是如此,但在我的案例中,升上 Anglar 15 前是可以運作;而升版之後,service 則不存在,無法從它身上執行 getName()

為什麼以前可以?

本文所謂的「以前」,即是未升上 Angular 15 前,當時 tsconfig.json 當中的 targetES2020

我們可以直接在 TypeScript Playground 進行實驗,選擇 TypeScript 版本為 v.4.8.4,並且在 TS Config 設定 target 為 ES2020。(TS 版本其實可以用最新的 5.1.x,因為關鍵在於 target,這邊只是盡量還原過去)

將 Target 設定為 ES2020
將 Target 設定為 ES2020

在右側 .JS 區塊可看到 tsc 轉譯後的 JavaScript ,變數賦值都移到 constructor 內部來處理,十分和諧。

為什麼現在不行?

升上 Angular 15 後,Angular CLI 會將 tsconfig.jsontarget 更改至 ES2022,並設定 useDefineForClassFieldsfalse (ref.)。依樣畫胡蘆,在 TS Playground 選擇 TypeScript 5.1.3,並設定 targetES2022

將 Target 設定為 ES2022

然後,你就得到一樣的結果 (咦)。

嗯… 其實你還要再進到 TS Config 當中,把 Language and Environment 當中的 useDefineForClassFields 勾起來 (註)!

勾選 Language and Environment 當中的 useDefineForClassFields 選項

之後可以發現:

Class 直接存在兩個成員(屬性)的宣告,一是 service,二是 greeting,相信這邊可以看出問題所在,也能夠體會所謂的怪味道。

顯然 gretting 在初始化的當下就會遇到問題,因為 service 其實還不存在,就算我們還沒反應過來,編輯器 (如 VS Code、TS Playground) 也老早發出警告,會直接給予提示:Property ‘service’ is used before its initialization.(2729)

註:依照官方文件的說明,設定欄位 useDefineForClassFields 在 target 為 ES2022 時,預設是 true。不排除是 TS Playground 以 Checkbox 來設計,可能造成沒勾為 false;有勾為 true。但總之要透過它實驗與實現時,記得先調整。

如何解決?

可以在 tsconfig 當中,於 compilerOptionsuseDefineForClassFields設定 為 false

或是調整程式的寫法,這也有合理性,畢竟其他語言不見得像 TypeScript 有提供 Parameter Properties 特性,宣告變數並在 Constructor 當中賦值的寫法也是能理解的。

例如,若不使用 Parameter Properties,會寫成這樣:

class Component {
private greeting; // 宣告
constructor(
private service: Service,
) {
this.greeting = 'Hi,' + this.service.getName(); // 賦值
}
}

迷霧呢?

其實看完上面,倒沒什麼迷霧,因實際上產生迷霧的根源是「設定不一致」。

由於 Repo 起草的早,早在手動引入 ng-packagr 作為打包工具就開始運作著,隨時間產生設定不一致,且升版過程也不容易被察覺。若是以 Angular CLI 推出的新方式,即透過 ng g library 的方式在同一個 repo 當中管理多個 libs,或許能迴避此問題。

是怎麼樣的不一致呢?ng-packagr 是可以透過 -- config 參數傳入指定的 tsconfig,若沒有指定則使用它的預設。在過去是不需要特別傳入,但如今 ng-packagr 的預設值已變成 ES2022,並且無特別指定 useDefineForClassFields (採預設 true)。同時,我們的 Repo 卻將 useDefineForClassFields 設定為 false ,也因此:開發時沒有感覺、ng serve 沒感覺,建置 library 卻出錯了,就是因為如此--開發與建置的設定是不一致的

小結

  • 若您的 target 為 ES2022,關心一下 useDefineForClassFields 的值為何、留意開發工具給您的提醒。
  • 若您使用 ng-packagr v16 以上(含),留意您是否需要傳遞 tsconfig 給它,因為它的 targetES2022,並且沒有 useDefineForClassFields=false。

--

--