TypeScript 編譯與設定:compilerOptions.strict 完全指南
簡介
在大型 JavaScript 專案中,型別安全是維護程式碼品質、降低錯誤率的關鍵。TypeScript 之所以受到廣大開發者青睞,主要是因為它的編譯器(tsc)提供了一系列可自訂的檢查規則,而 compilerOptions.strict 正是其中最具影響力的一把「安全開關」。
開啟 strict 後,編譯器會一次啟用多項嚴格模式(strictNullChecks、strictFunctionTypes、strictPropertyInitialization、noImplicitAny、noImplicitThis、alwaysStrict 等),讓 每一行程式碼都必須在型別上明確、合理。雖然在開發初期會遇到較多的型別錯誤,但長遠來看,這能顯著減少執行時的不可預期行為,提升團隊協作的可預測性。
本篇文章將逐一說明 strict 的組成、實作方式與常見陷阱,並提供實務範例,協助你在專案中安全、有效地啟用這項功能。
核心概念
1. strict 是什麼?
strict 本身不是一個獨立的檢查規則,而是一個總開關,等同於同時開啟以下子選項:
| 子選項 | 功能說明 |
|---|---|
strictNullChecks |
嚴格的 null 與 undefined 判斷。必須明確處理 null/undefined,否則會產生型別錯誤。 |
strictFunctionTypes |
函式參數雙向協變檢查,確保函式型別相容性更精確。 |
strictPropertyInitialization |
類別屬性必須在建構子或宣告時初始化,避免未賦值屬性造成的執行時錯誤。 |
noImplicitAny |
隱式 any 會被視為錯誤,要求開發者必須給予明確型別。 |
noImplicitThis |
隱式 this 會被視為錯誤,避免在函式內部誤用 this。 |
alwaysStrict |
自動加入 "use strict" 到輸出檔,確保 JavaScript 嚴格模式。 |
strictBindCallApply |
bind、call、apply 的型別檢查更嚴格。 |
strictSaveFunctionTypes(自 TS 5.0 起) |
保存函式型別 以提升型別推斷的準確度。 |
只要在 tsconfig.json 中將 strict: true,以上全部會一次啟用。若要微調,可個別關閉("strictNullChecks": false)或自行開啟其他嚴格選項。
2. 為什麼要開啟 strict?
- 提前捕捉錯誤:大多數 bug(例如未處理
null、錯誤的函式參數)會在編譯階段被檢測出,減少部署後的緊急修復。 - 提升自動完成與型別推斷:嚴格模式讓編輯器(VS Code、WebStorm)能更精準地提供 IntelliSense,提升開發效率。
- 文件化程式碼意圖:每個變數、函式的型別都必須明確,讓新加入的同事能快速理解程式邏輯。
- 支援未來的 JavaScript 標準:
alwaysStrict讓產出檔自動使用 ES5+ 的嚴格模式,減少舊版行為的衝突。
3. 如何在專案中啟用 strict
在專案根目錄建立(或編輯)tsconfig.json,加入以下設定:
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"strict": true, // 開啟全部嚴格檢查
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
小技巧:若團隊尚未全部適應,可以先設定
"strict": false,然後逐項開啟(如noImplicitAny)作為漸進式遷移的策略。
程式碼範例
以下示範在開啟 strict 前後的差異,讓你直觀感受到每個子選項的作用。
範例 1:strictNullChecks
// 沒有 strictNullChecks 時
function greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
greet(null); // ✅ 編譯通過,執行時會拋出錯誤
// 開啟 strictNullChecks 後
function greet(name: string) {
// 編譯器會提示 name 可能為 null
console.log(`Hello, ${name.toUpperCase()}`);
}
greet(null); // ❌ 編譯錯誤:Argument of type 'null' is not assignable to parameter of type 'string'.
解決方式:使用聯合型別或非空斷言:
function greet(name: string | null) {
if (name) {
console.log(`Hello, ${name.toUpperCase()}`);
} else {
console.log('Hello, Guest');
}
}
範例 2:noImplicitAny
function add(a, b) {
return a + b;
}
add(1, 2); // ✅ 沒有錯誤,a、b 被推斷為 any
// 開啟 noImplicitAny 後
function add(a, b) {
return a + b;
}
// ❌ 錯誤:Parameter 'a' implicitly has an 'any' type.
解決方式:明確指定型別或使用泛型:
function add(a: number, b: number): number {
return a + b;
}
範例 3:strictPropertyInitialization
class User {
name: string; // 未初始化
age?: number; // 可選屬性
constructor(name: string) {
this.name = name; // 只在建構子裡賦值
}
}
在 strictPropertyInitialization 開啟時,若有屬性未在宣告或建構子中保證初始化,編譯會報錯。解決方法:
class User {
name: string;
age?: number;
isActive: boolean = false; // 直接初始化
constructor(name: string) {
this.name = name;
}
}
範例 4:strictFunctionTypes
type Handler = (event: MouseEvent) => void;
function register(handler: Handler) {
// ...
}
// 允許傳入更寬鬆的型別(舊行為)
register((e: Event) => console.log(e.type)); // ✅
在 strictFunctionTypes 開啟後,上例會 編譯錯誤,因為 Event 不是 MouseEvent 的子型別。正確寫法:
register((e: MouseEvent) => console.log(e.button));
範例 5:noImplicitThis
function Counter(this: { count: number }) {
this.count++;
}
Counter(); // ❌ 編譯錯誤:'this' implicitly has type 'any'
解決方式:使用箭頭函式或明確宣告 this 型別:
function Counter(this: { count: number }) {
this.count++;
}
const obj = { count: 0 };
Counter.call(obj); // 正確
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
過度使用 any |
為了快速通過編譯,直接宣告 any,失去型別保護。 |
使用 unknown 或 具體型別,必要時搭配型別斷言。 |
忽略 null/undefined |
忘記在函式或屬性上加入 ` | null/ |
| 類別屬性未初始化 | strictPropertyInitialization 會阻止未賦值的屬性。 |
在宣告時給予預設值,或在建構子裡完整賦值;若屬性確實可延遲初始化,可加上 ! 非空斷言(慎用)。 |
| 函式參數型別不匹配 | strictFunctionTypes 讓函式參數的協變檢查更嚴格。 |
使用泛型或 overload,讓型別更貼近實際呼叫情境。 |
this 失去型別 |
在普通函式中使用 this,編譯器無法推斷。 |
改用箭頭函式(自動綁定 this)或 在函式簽名中明確宣告 this 型別。 |
最佳實踐清單
- 從
noImplicitAny開始:先解決隱式 any,再逐步開啟其他子選項。 - 使用
--watch搭配 IDE:即時看到錯誤,避免一次性大量修正造成的焦慮。 - 建立型別工具函式:例如
assertIsString(value: unknown): asserts value is string,可以在執行時保證型別安全。 - 在第三方庫上使用
@types:若缺少型別宣告,先自行撰寫或使用declare module包裝。 - CI 中加入
tsc --noEmit:確保每次提交都通過嚴格檢查,防止錯誤進入主分支。
實際應用場景
1. 大型前端單頁應用(SPA)
在 React 或 Vue 專案中,資料流經常在多層元件間傳遞。開啟 strictNullChecks 能讓你在 props、state、context 中明確標示哪些屬性可能為 null,避免渲染時的空指標例外。
type UserCardProps = {
name: string;
avatarUrl?: string | null; // 可能為 null
};
export const UserCard: React.FC<UserCardProps> = ({ name, avatarUrl }) => (
<div>
<h2>{name}</h2>
{avatarUrl ? <img src={avatarUrl} alt={name} /> : <DefaultAvatar />}
</div>
);
2. Node.js 後端服務
在 Express 中,請求物件的屬性(例如 req.body)往往是 any。透過 noImplicitAny 和自訂型別(interface RequestBody { id: number; }),可以在路由處理函式裡直接取得型別提示,減少手寫校驗的負擔。
import { Request, Response } from 'express';
interface CreateUserBody {
name: string;
email: string;
}
app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response) => {
const { name, email } = req.body; // 完全受型別保護
// ...
});
3. 套件開發(Library)
若你正為 npm 發布 TypeScript 套件,嚴格模式是品質保證。使用者在安裝你的套件後,若開啟 strict,他們的編譯器會直接檢測到不相容的型別,提升套件的可用性與信任度。
// package.json 中的 "types"
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
}
}
總結
compilerOptions.strict是 TypeScript 提供的 全方位型別安全總開關,一次啟用即能開啟多項嚴格檢查。- 開啟後,你會在開發初期看到大量編譯錯誤,這是 早期捕捉問題、提升程式碼品質 的好機會。
- 透過 分步啟用、結合 IDE 錯誤提示、在 CI 中加入檢查,可以平滑地將既有專案遷移至嚴格模式。
- 在實務上,
strict能防止常見的null/undefined錯誤、隱式any、未初始化屬性等問題,特別適用於 大型前端框架、Node.js 後端服務與套件開發。
建議:從
noImplicitAny開始逐步加強,最終將strict: true作為專案的預設設定,讓型別安全成為團隊開發文化的一部分。
祝你在 TypeScript 的型別世界裡玩得開心,寫出更安全、更可靠的程式碼! 🚀