本文 AI 產出,尚未審核

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)或 esbuildvite,即時 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,造成檔案重複或衝突 確保 noEmittrue,讓 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']

最佳實踐

  1. 分離型別檢查與編譯tsc --noEmit + Babel,保持每個工具專注於自己的職責。
  2. 在 CI 中加入兩段指令npm run type-check && npm run build,確保每次提交都通過型別與語法檢查。
  3. 使用 babel-plugin-module-resolver 統一別名,避免 tsconfig 與 Babel 別名不一致的問題。
  4. 設定 sourceMaps@babel/preset-typescript 會自動傳遞 sourceMaps 設定),方便除錯。

實際應用場景

場景 為什麼選 Babel + TypeScript 典型設定
大型單頁應用(SPA) 需要大量 Babel 插件(如 styled-componentsreact-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 時希望提供 ESMCJS 兩種格式,且讓使用者自行決定 polyfill 使用 babel-cli--out-dir + @babel/preset-envmodules: 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.json vs Babel 別名),可避免常見的建置錯誤。
  • CI、CI/CD 流程中同時執行型別檢查與 Babel 打包,保證每次部署的品質。

掌握上述概念與實作方式,你就能在 React、Vue、Node.js 等多種環境下,使用 最先進的語法與型別安全,同時享受 Babel 生態的彈性與效能。祝開發順利,寫出乾淨、可維護的 TypeScript 程式碼!