TypeScript 與 Babel 整合:使用 @babel/preset-typescript 的完整指南
簡介
在前端開發的生態系中,Babel 已成為「把新語法轉譯成瀏覽器可執行程式碼」的事實標準,而 TypeScript 則提供了靜態型別檢查、先進的 IDE 體驗與未來語法的提前使用。
將兩者結合,我們可以同時享有 Babel 的插件生態(如 polyfill、React JSX、ESNext)與 TypeScript 的型別安全,並且在同一個建置流程中完成 編譯 + 轉譯。
本篇文章將說明為什麼以及怎麼在專案中使用 @babel/preset-typescript,從安裝、設定到實務範例,帶你一步步建立 可維護、效能佳 的 TypeScript + Babel 工作流。
核心概念
1. Babel vs. tsc:職責分工
| 功能 | tsc(TypeScript Compiler) |
Babel (@babel/preset-typescript) |
|---|---|---|
| 型別檢查 | ✅ 完整的型別系統 | ❌ 只做語法層面的轉譯 |
| 產出 ES5/ES6 目標 | ✅ 透過 target 設定 |
✅ 由 Babel 的 @babel/preset-env 處理 |
| 支援 Babel 插件(如 proposal‑class‑properties) | ❌ 需要額外設定 | ✅ 可直接使用 |
| 設定檔簡潔度 | tsconfig.json |
.babelrc / babel.config.js |
結論:在需要大量 Babel 插件或想統一前端與 Node.js 的轉譯流程時,使用 Babel 來處理 TypeScript 是更彈性的選擇;仍然保留 tsc --noEmit 作為型別檢查工具。
2. 為什麼需要 @babel/preset-typescript
- 只負責語法轉譯:把
.ts/.tsx直接轉成 JavaScript,讓 Babel 接手後續的 polyfill、模組化等工作。 - 與其他 preset 完美共存:如
@babel/preset-react、@babel/preset-env,可以一次完成 JSX、ESNext 與 TypeScript 的處理。 - 增強開發體驗:結合
babel-loader(Webpack)或esbuild、vite,即時 HMR、快速重建。
3. 安裝與基本設定
# 使用 npm
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-typescript
# 若使用 React
npm install --save-dev @babel/preset-react
建立 babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: [
// 1️⃣ 先轉譯 TypeScript
['@babel/preset-typescript', {
// 只保留型別資訊,不產生 .d.ts
allExtensions: true,
isTSX: true, // 若有 .tsx 檔案
}],
// 2️⃣ 再處理最新的 JavaScript 語法
['@babel/preset-env', {
targets: '> 0.25%, not dead',
useBuiltIns: 'usage',
corejs: 3,
}],
// 3️⃣ 若是 React 專案
'@babel/preset-react',
],
plugins: [
// 例:class properties、optional chaining 等
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
],
};
};
小技巧:
allExtensions: true讓 Babel 同時處理.ts、.tsx,免去在 Webpack 中額外指定test: /\.(ts|tsx)$/。
4. 透過 tsc --noEmit 只做型別檢查
// tsconfig.json
{
"compilerOptions": {
"noEmit": true, // 只檢查,不產出檔案
"strict": true,
"target": "ESNext",
"module": "ESNext",
"jsx": "preserve"
},
"include": ["src/**/*"]
}
在 package.json 加入腳本:
{
"scripts": {
"type-check": "tsc --noEmit",
"build": "babel src --out-dir dist --extensions \".ts,.tsx\""
}
}
這樣 npm run type-check 只跑型別檢查,而 npm run build 完成編譯與打包。
5. 程式碼範例
以下示範 4 個常見情境,說明 Babel + TypeScript 的實作方式。
範例 1:基本型別與 ESNext 語法
// src/util.ts
export const greet = (name: string): string => {
// 使用 optional chaining、nullish coalescing
const upper = name?.toUpperCase() ?? 'UNKNOWN';
return `Hello, ${upper}!`;
};
編譯結果(由 Babel 產出):
export const greet = name => {
const upper = name?.toUpperCase() ?? 'UNKNOWN';
return `Hello, ${upper}!`;
};
重點:型別
: string已在tsc --noEmit階段被檢查,轉譯後的程式碼只保留執行時需要的部分。
範例 2:使用 JSX(React + TSX)
// src/components/Counter.tsx
import React, { useState } from 'react';
type Props = {
/** 初始值 */
start?: number;
};
export const Counter: React.FC<Props> = ({ start = 0 }) => {
const [count, setCount] = useState<number>(start);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
};
Babel 會先把 TSX 轉成 JSX,再交給 @babel/preset-react 產生 React.createElement 呼叫,最終產出可在瀏覽器執行的程式碼。
範例 3:自訂裝飾器(Decorator)
注意:裝飾器仍屬於 Stage‑2 提案,需要額外插件。
// src/decorators/log.ts
export function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
return original.apply(this, args);
};
return descriptor;
}
使用方式:
import { Log } from './decorators/log';
class Service {
@Log
fetch(id: number) {
// ... fetch logic
return { id };
}
}
Babel 設定(加入 @babel/plugin-proposal-decorators):
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
// 其他插件...
];
範例 4:與第三方庫共用型別(例如 lodash)
// src/helpers/arr.ts
import { chunk } from 'lodash';
export const splitIntoPairs = <T>(arr: T[]): T[][] => {
// 使用 generic + lodash 的型別定義
return chunk(arr, 2);
};
關鍵點:即使 Babel 不會檢查型別,tsc --noEmit 仍會根據 @types/lodash 進行檢查,確保 chunk 的呼叫參數正確。
常見陷阱與最佳實踐
| 陷阱 | 可能的症狀 | 解決方案 |
|---|---|---|
忘記在 tsconfig.json 設定 noEmit:true |
npm run build 同時產生 .js 與 .d.ts,造成檔案重複或衝突 |
確保 noEmit 為 true,讓 Babel 完全負責輸出 |
| Babel 直接忽略型別錯誤 | 程式碼可編譯成功,但執行時出現隱藏的型別錯誤 | 在 CI 中加入 npm run type-check,或使用 husky pre‑commit hook 強制檢查 |
使用 import type 卻未升級 Babel |
import type 會被當成普通 import,導致多餘的執行時代碼 |
需要 Babel 7.14+(內建支援 import type) |
裝飾器與 @babel/preset-typescript 不相容 |
裝飾器編譯失敗或產生錯誤的原型鏈 | 加入 @babel/plugin-proposal-decorators 並使用 { legacy: true } 配置 |
| 多檔案副檔名未被 Babel 捕捉 | .tsx 檔案仍走舊的 ts-loader,導致兩套編譯器同時運作 |
在 babel.config.js 設定 allExtensions: true,或在 webpack 中明確列出 extensions: ['.js', '.ts', '.tsx'] |
最佳實踐:
- 分離型別檢查與編譯:
tsc --noEmit+ Babel,保持每個工具專注於自己的職責。 - 在 CI 中加入兩段指令:
npm run type-check && npm run build,確保每次提交都通過型別與語法檢查。 - 使用
babel-plugin-module-resolver統一別名,避免tsconfig與 Babel 別名不一致的問題。 - 設定
sourceMaps(@babel/preset-typescript會自動傳遞sourceMaps設定),方便除錯。
實際應用場景
| 場景 | 為什麼選 Babel + TypeScript | 典型設定 |
|---|---|---|
| 大型單頁應用(SPA) | 需要大量 Babel 插件(如 styled-components、react-refresh)同時保留型別安全 |
@babel/preset-react + @babel/preset-typescript + react-refresh/babel |
| Node.js 微服務 | 目標平台為 Node 14+,只需要把 TypeScript 轉成符合 Node 的語法,且想使用最新的 ES 模組特性 | targets: { node: "14" } + @babel/preset-typescript |
| 跨平台函式庫(library) | 發佈到 npm 時希望提供 ESM、CJS 兩種格式,且讓使用者自行決定 polyfill | 使用 babel-cli 的 --out-dir + @babel/preset-env 的 modules: false |
| React Native | RN 內建 Babel,直接在 metro.config.js 加入 @babel/preset-typescript 即可 |
metro.config.js 中的 transformer.babelTransformerPath 設定 |
總結
- Babel +
@babel/preset-typescript為前端開發提供了 「語法轉譯 + 插件生態」 的完整解決方案,同時保留 TypeScript 的靜態型別檢查。 - 透過
tsc --noEmit只做型別檢查,讓兩套工具各司其職,減少重複編譯、提升建置速度。 - 注意 插件相容性(裝飾器、
import type)以及 設定一致性(tsconfig.jsonvs Babel 別名),可避免常見的建置錯誤。 - 在 CI、CI/CD 流程中同時執行型別檢查與 Babel 打包,保證每次部署的品質。
掌握上述概念與實作方式,你就能在 React、Vue、Node.js 等多種環境下,使用 最先進的語法與型別安全,同時享受 Babel 生態的彈性與效能。祝開發順利,寫出乾淨、可維護的 TypeScript 程式碼!