本文 AI 產出,尚未審核

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.definePropertyArray.prototype.forEach
"ES6" / "ES2015" ECMAScript 2015 let/constarrow functionclasstemplate string
"ES2016" ECMAScript 2016 Array.prototype.includes、指數運算子 **
"ES2017" ECMAScript 2017 async/awaitObject.entries
"ES2018" ECMAScript 2018 rest/spread 於物件、async iterators
"ES2019" ECMAScript 2019 Array.prototype.flatObject.fromEntries
"ES2020" ECMAScript 2020 BigIntoptional chainingnullish coalescing
"ES2021" ECMAScript 2021 String.prototype.replaceAllPromise.any
"ES2022" ECMAScript 2022 class fieldsprivate methods
"ESNext" 最新標準(未正式發布) 允許使用尚未正式納入規範的特性(需配合相容環境)

Tiptsc --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:提供全域型別定義(DOMES2020 等),若 target 較低卻使用較新 lib,仍需要自行 polyfill。
  • downlevelIteration:在 target 為 ES5 時,若想保留 for...of 迭代的語意,需要開啟此選項。

程式碼範例

下面的範例展示同一段 TypeScript 程式碼,在不同 target 設定下的編譯結果。

:以下編譯指令皆假設已在專案根目錄建立 tsconfig.json,僅修改 target 後執行 npx tsc

範例 1 – let/constclass

// 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 直接保留 classconst,程式碼更簡潔。


範例 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)。

結論:若專案需要 BigInttarget 必須至少是 ES2020;否則 TypeScript 會直接錯誤而非降級。


範例 5 – 結合 moduletarget(示範 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/exportasyncclass 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
moduletarget 不匹配 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

建議的設定流程

  1. 確定支援環境:使用 browserslist、Node.js LTS 版本或企業內部支援表格。
  2. 選擇最低可接受的 target:以支援環境的最低 ES 版本為基礎。
  3. 對應 lib:讓型別系統與目標語法保持一致。
  4. 測試產出:在 CI 中加入 npm run build && node dist/... 或在瀏覽器中執行自動化測試,確保編譯結果可執行。
  5. 必要時加 polyfill:若選擇較低 target,但仍想使用新語法,透過 core-jsregenerator-runtime 等補齊缺失功能。

實際應用場景

1. 企業內部系統(支援 IE11)

  • target"ES5"
  • module"CommonJS"(Node.js 伺服器端)或 ["AMD"](舊版前端)
  • lib["ES5", "DOM"]
  • 額外:加入 core-jsregenerator-runtime,確保 PromiseArray.from 等新特性可在 IE11 執行。

2. 單頁應用(SPA)使用 Vite/ESBuild

  • target"ES2020"(或 ESNext
  • module"ESNext"(讓 Vite 能直接使用原生 ES 模組)
  • lib["ES2020", "DOM"]
  • 好處:保留 optional chainingnullish coalescingBigInt,減少編譯膨脹,提升執行效能。

3. Node.js 後端服務(LTS 16+)

  • target"ES2019"(Node 16 完全支援)
  • module"CommonJS"(預設)或 "ESNext"(若使用 ES 模組)
  • lib["ES2019"]
  • 情境:可直接使用 async/awaitObject.fromEntries,不必額外安裝 Babel。

4. 跨平台套件(npm library)

  • target"ES5"(保證最廣相容性)
  • module:同時輸出 CommonJSESNext(透過 tsc --module 多次編譯或使用 rollup
  • lib["ES5", "DOM"](若涉及瀏覽器 API)
  • 目的:讓使用者能在任何環境下直接 import,而不必自行轉譯。

總結

  • compilerOptions.target 決定了 TypeScript 編譯後產出的 JavaScript 版本,直接影響程式碼的可執行性與效能。
  • 依照 支援環境(瀏覽器或 Node)選擇合適的 target,配合 modulelibdownlevelIteration 等選項,才能避免執行時錯誤或不必要的 polyfill。
  • 實務上,企業舊系統仍會使用 ES5,而新專案則傾向 ES2020ESNext,搭配現代打包工具即可保持程式碼簡潔且效能最佳。
  • 常見陷阱包括 目標環境不支援、lib 不同步、module/target 不相容,透過 瀏覽器清單、CI 測試與適當的 polyfill 可輕鬆化解。

掌握 target 的設定原理與實務應用,能讓你在開發 TypeScript 專案時,既保證相容性,又不失現代語法的便利與效能。祝開發順利,寫出乾淨、可維護的程式碼!