TypeScript 編譯設定 – compilerOptions.target
簡介
在使用 TypeScript 開發時,最常會碰到的設定之一就是 compilerOptions.target。它決定了 TypeScript 編譯器(tsc) 最終產出的 JavaScript 版本,也就是瀏覽器或 Node.js 能夠直接執行的程式碼。
選擇不當的 target 可能會導致程式在舊版環境跑不出來,或是產出過於老舊的語法,浪費了現代瀏覽器提供的效能優化。相反地,若設定過高,則在不支援該語法的環境中會拋出錯誤,必須再額外加上 polyfill 或 Babel 轉譯。
因此,了解 target 的意義、可選的值以及實務上應該如何挑選,是每位 TypeScript 開發者在建構專案時的必備功課。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 target 的使用方式。
核心概念
1. target 能接受哪些值?
| 值(字串) | 產出 ES 版本 | 主要特性 |
|---|---|---|
"ES3" |
ECMAScript 3 | 最老舊的語法,僅適用於極度舊版瀏覽器 |
"ES5" |
ECMAScript 5 | 支援 Object.defineProperty、Array.prototype.forEach 等 |
"ES6" / "ES2015" |
ECMAScript 2015 | let/const、arrow function、class、template string |
"ES2016" |
ECMAScript 2016 | Array.prototype.includes、指數運算子 ** |
"ES2017" |
ECMAScript 2017 | async/await、Object.entries |
"ES2018" |
ECMAScript 2018 | rest/spread 於物件、async iterators |
"ES2019" |
ECMAScript 2019 | Array.prototype.flat、Object.fromEntries |
"ES2020" |
ECMAScript 2020 | BigInt、optional chaining、nullish coalescing |
"ES2021" |
ECMAScript 2021 | String.prototype.replaceAll、Promise.any |
"ES2022" |
ECMAScript 2022 | class fields、private methods |
"ESNext" |
最新標準(未正式發布) | 允許使用尚未正式納入規範的特性(需配合相容環境) |
Tip:
tsc --init產生的tsconfig.json預設 target 為"ES3",大多數專案會自行改成"ES5"或更高。
2. 為什麼 target 會影響編譯結果?
TypeScript 的型別檢查在編譯階段完成,之後會把 TypeScript 語法 轉換成 對應的 JavaScript 語法。
舉例來說:
| TypeScript 語法 | target = ES5 產出 | target = ES2015 產出 |
|---|---|---|
const x = 10; |
var x = 10; |
const x = 10; |
let y = 1; |
var y = 1; |
let y = 1; |
class Person {} |
IIFE + prototype | class Person {} |
async function foo(){} |
generator + __awaiter | async function foo(){} |
若目標環境已支援 ES2015+,保留原生語法可以讓程式碼更簡潔、效能更好;若目標環境僅支援 ES5,編譯器會自動把新語法降級(down‑level)成等價的舊語法。
3. target 與其他編譯選項的關係
module:決定模組系統(CommonJS、ESNext、AMD …)。某些module只能搭配特定的target(例如ESNext+module: ESNext)。lib:提供全域型別定義(DOM、ES2020等),若target較低卻使用較新lib,仍需要自行 polyfill。downlevelIteration:在target為 ES5 時,若想保留for...of迭代的語意,需要開啟此選項。
程式碼範例
下面的範例展示同一段 TypeScript 程式碼,在不同 target 設定下的編譯結果。
註:以下編譯指令皆假設已在專案根目錄建立
tsconfig.json,僅修改target後執行npx tsc。
範例 1 – let/const 與 class
// file: demo1.ts
class Counter {
private _count = 0;
increment() {
this._count++;
}
get value() {
return this._count;
}
}
const c = new Counter();
c.increment();
console.log(c.value);
target = "ES5"(編譯結果)
var Counter = /** @class */ (function () {
function Counter() {
this._count = 0;
}
Counter.prototype.increment = function () {
this._count++;
};
Object.defineProperty(Counter.prototype, "value", {
get: function () {
return this._count;
},
enumerable: false,
configurable: true
});
return Counter;
}());
var c = new Counter();
c.increment();
console.log(c.value);
target = "ES2015"(編譯結果)
class Counter {
constructor() {
this._count = 0;
}
increment() {
this._count++;
}
get value() {
return this._count;
}
}
const c = new Counter();
c.increment();
console.log(c.value);
觀察:
ES5版使用 IIFE +Object.defineProperty,而ES2015直接保留class與const,程式碼更簡潔。
範例 2 – async/await
// file: demo2.ts
async function fetchData(url: string) {
const response = await fetch(url);
return response.json();
}
fetchData('https://api.example.com/data')
.then(data => console.log(data));
target = "ES5"(編譯結果)
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
// ... 省略的 helper 程式碼
};
function fetchData(url) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetch(url);
return response.json();
});
}
fetchData('https://api.example.com/data')
.then(data => console.log(data));
target = "ES2017"(編譯結果)
async function fetchData(url) {
const response = await fetch(url);
return response.json();
}
fetchData('https://api.example.com/data')
.then(data => console.log(data));
說明:在
ES5目標下,編譯器會注入__awaiter輔助函式,將async/await轉譯為 generator + Promise。若目標支援async(ES2017+),則保持原生寫法。
範例 3 – 可選鏈結(Optional Chaining)與空值合併(Nullish Coalescing)
// file: demo3.ts
interface User {
profile?: {
name?: string;
};
}
const user: User = {};
const name = user.profile?.name ?? 'Anonymous';
console.log(name);
target = "ES2020"(編譯結果)
var user = {};
var name = user.profile?.name ?? 'Anonymous';
console.log(name);
target = "ES5"(編譯結果)
var user = {};
var name = (user.profile === null || user.profile === void 0 ? void 0 : user.profile.name) !== null && (user.profile === null || user.profile === void 0 ? void 0 : user.profile.name) !== void 0 ? user.profile.name : 'Anonymous';
console.log(name);
重點:
ES2020直接保留?.與??,而ES5必須展開成繁雜的三元運算式。若不想看到這種「膨脹」的程式碼,建議在支援的環境下使用較新 target。
範例 4 – BigInt(ES2020+)
// file: demo4.ts
const big: bigint = 9007199254740991n + 1n;
console.log(big);
target = "ES2020"(編譯結果)
const big = 9007199254740991n + 1n;
console.log(big);
target = "ES5"(編譯結果)
// 編譯器會報錯:Cannot find name 'bigint' because target is too low.
// 必須自行降級或使用第三方 polyfill(如 JSBI)。
結論:若專案需要
BigInt,target必須至少是ES2020;否則 TypeScript 會直接錯誤而非降級。
範例 5 – 結合 module 與 target(示範 ESNext)
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"outDir": "./dist",
"sourceMap": true
}
}
// file: demo5.ts
export async function hello(name: string) {
return `Hello, ${name}!`;
}
編譯結果(dist/demo5.js):
export async function hello(name) {
return `Hello, ${name}!`;
}
說明:使用
ESNext時,編譯器不會對語法做任何降級,直接保留原生import/export、async、class fields等,適合配合現代打包工具(如 Vite、Webpack 5)或直接在支援的瀏覽器環境中使用。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方式 / 最佳實踐 |
|---|---|---|
| 目標瀏覽器不支援 target 所產出的語法 | 設定 target: "ES2020",但仍需支援 IE11。 |
1️⃣ 以 targets(如 browserslist)決定最低支援版本;2️⃣ 使用 target: "ES5" 搭配 polyfill(core-js、regenerator-runtime)。 |
忘記同步調整 lib |
target: "ES2020" 但 lib 仍是 ["DOM"],導致 BigInt 等型別報錯。 |
在 tsconfig.json 中加入 lib: ["ES2020", "DOM"],或直接使用 --lib es2020,dom。 |
module 與 target 不匹配 |
module: "CommonJS" 搭配 target: "ESNext",在某些工具(如 ts-node)會產生 import/export 不能被正確解析。 |
依照執行環境選擇相容組合: • Node.js (>=12) → module: "CommonJS" + target: "ES2019" • 前端 bundler → module: "ESNext" + target: "ES2020" |
過度依賴 ESNext |
直接寫 ?.、??、class fields,卻在舊版 Chrome 中執行失敗。 |
使用 browserslist + @babel/preset-env 讓打包工具自動降級,或在 tsconfig 中設定較保守的 target。 |
downlevelIteration 忘記開啟 |
在 target: "ES5" 時使用 for...of 迭代 Map/Set,結果會變成遍歷空物件。 |
若需要正確的迭代行為,加入 "downlevelIteration": true。 |
建議的設定流程
- 確定支援環境:使用
browserslist、Node.js LTS 版本或企業內部支援表格。 - 選擇最低可接受的 target:以支援環境的最低 ES 版本為基礎。
- 對應
lib:讓型別系統與目標語法保持一致。 - 測試產出:在 CI 中加入
npm run build && node dist/...或在瀏覽器中執行自動化測試,確保編譯結果可執行。 - 必要時加 polyfill:若選擇較低 target,但仍想使用新語法,透過
core-js、regenerator-runtime等補齊缺失功能。
實際應用場景
1. 企業內部系統(支援 IE11)
- target:
"ES5" - module:
"CommonJS"(Node.js 伺服器端)或["AMD"](舊版前端) - lib:
["ES5", "DOM"] - 額外:加入
core-js與regenerator-runtime,確保Promise、Array.from等新特性可在 IE11 執行。
2. 單頁應用(SPA)使用 Vite/ESBuild
- target:
"ES2020"(或ESNext) - module:
"ESNext"(讓 Vite 能直接使用原生 ES 模組) - lib:
["ES2020", "DOM"] - 好處:保留
optional chaining、nullish coalescing、BigInt,減少編譯膨脹,提升執行效能。
3. Node.js 後端服務(LTS 16+)
- target:
"ES2019"(Node 16 完全支援) - module:
"CommonJS"(預設)或"ESNext"(若使用 ES 模組) - lib:
["ES2019"] - 情境:可直接使用
async/await、Object.fromEntries,不必額外安裝 Babel。
4. 跨平台套件(npm library)
- target:
"ES5"(保證最廣相容性) - module:同時輸出
CommonJS與ESNext(透過tsc --module多次編譯或使用rollup) - lib:
["ES5", "DOM"](若涉及瀏覽器 API) - 目的:讓使用者能在任何環境下直接
import,而不必自行轉譯。
總結
compilerOptions.target決定了 TypeScript 編譯後產出的 JavaScript 版本,直接影響程式碼的可執行性與效能。- 依照 支援環境(瀏覽器或 Node)選擇合適的 target,配合
module、lib、downlevelIteration等選項,才能避免執行時錯誤或不必要的 polyfill。 - 實務上,企業舊系統仍會使用
ES5,而新專案則傾向ES2020或ESNext,搭配現代打包工具即可保持程式碼簡潔且效能最佳。 - 常見陷阱包括 目標環境不支援、
lib不同步、module/target 不相容,透過 瀏覽器清單、CI 測試與適當的 polyfill 可輕鬆化解。
掌握 target 的設定原理與實務應用,能讓你在開發 TypeScript 專案時,既保證相容性,又不失現代語法的便利與效能。祝開發順利,寫出乾淨、可維護的程式碼!