釋出 TypeScript 包

本文專門介紹有關 TypeScript 釋出的事項。“釋出”是指透過 npm(或其他包管理器)以包的形式分發;這與編譯要在生產環境中執行的應用/伺服器(例如 PWA 和/或端點伺服器)無關。

一些需要注意的重要事項

  • 釋出包中的所有內容都適用於此。

    • main 這樣的欄位作用於*已釋出*的內容,因此當 TypeScript 原始碼轉譯為 JavaScript 時,JavaScript 是已釋出的內容,而 main 會指向一個帶有 JavaScript 副檔名的 JavaScript 檔案(例如 main.ts"main": "main.js")。

    • scripts.test 這樣的欄位作用於原始碼,因此它們會使用原始碼的副檔名(例如 "test": "node --test './src/**/*.test.ts')。

  • Node 透過一個名為“型別剝離”的過程執行 TypeScript 程式碼,其中 Node(透過 Amaro)移除 TypeScript 特定的語法,留下原生 JavaScript(Node 已經理解)。此行為在 Node 22.18.0 及更高版本中預設啟用。

    • Node **不會**剝離 node_modules 中的型別,因為這可能對官方 TypeScript 編譯器(tsc)和 VS Code 的部分功能造成嚴重的效能問題,所以 TypeScript 維護者希望阻止人們釋出原始的 TypeScript,至少目前是這樣。
  • 在 Node 中使用 TypeScript 特定的功能(如 enum)仍然需要一個標誌(--experimental-transform-types)。無論如何,對於這些功能通常有更好的替代方案。

    • 為確保不包含 TypeScript 特定的功能(這樣你的程式碼就可以直接在 Node 中執行),請在 TypeScript 5.8 及更高版本中設定 erasableSyntaxOnly 配置選項。
  • 使用 Dependabot 來保持你的依賴項最新,包括 GitHub Actions 中的依賴。這是一個非常容易“設定後即忘”的配置。

  • .nvmrc 來自 nvm,一個用於 Node 的多版本管理器。它允許你指定專案通常應使用的 Node 版本。

一個程式碼倉庫的目錄概覽可能如下所示:

example-ts-pkg/
├ .github/
│ ├ workflows/
│ │ ├ ci.yml
│ │ └ publish.yml
│ └ dependabot.yml
├ src/
│ ├ foo.fixture.js
│ ├ main.ts
│ ├ main.test.ts
│ ├ some-util.ts
│ └ some-util.test.ts
├ LICENSE
├ package.json
├ README.md
└ tsconfig.json

其已釋出包的目錄概覽可能如下所示:

example-ts-pkg/
├ LICENSE
├ main.d.ts
├ main.d.ts.map
├ main.js
├ package.json
├ README.md
├ some-util.d.ts
├ some-util.d.ts.map
└ some-util.js

關於目錄組織的說明:放置測試有幾種常見的做法。最小知識原則建議將它們共置(放在實現檔案的旁邊)。有時,這在同一個目錄中,或在一個像 __test__ 這樣的抽屜目錄中(也與實現檔案相鄰,“檔案共置但隔離”)。或者,一些人選擇建立一個與 src/ 平級的 test/ 目錄(“‘src’和‘test’完全隔離”),其結構可以是映象的,也可以是一個“雜物抽屜”。

如何處理你的型別

像對待測試一樣對待型別

型別的目的是警告某個實現將無法正常工作

const  = 'a';
const bar: number = 1 + ;
Type 'string' is not assignable to type 'number'.

TypeScript 已經警告說上面的程式碼不會按預期執行,就像單元測試警告程式碼不會按預期執行一樣。它們是互補的,驗證不同的東西——你應該兩者都有。

你的編輯器(例如 VS Code)可能內建了對 TypeScript 的支援,會在你工作時顯示錯誤。如果沒有,或者你錯過了這些錯誤,CI 會為你提供保障。

下面的 GitHub Action 設定了一個 CI 任務,自動檢查(並要求)合併到 main 分支的 PR 的型別檢查透過。

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: Tests

on:
  pull_request:
    branches: ['*']

jobs:
  check-types:
    # Separate these from tests because
    # they are platform and node-version independent
    # and need be run only once.

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'
      - name: npm clean install
        run: npm ci
      # You may want to run a lint check here too
      - run: node --run types:check

  get-matrix:
    # Automatically pick active LTS versions
    runs-on: ubuntu-latest
    outputs:
      latest: ${{ steps.set-matrix.outputs.requireds }}
    steps:
      - uses: ljharb/actions/node/matrix@main
        id: set-matrix
        with:
          versionsAsRoot: true
          type: majors
          preset: '>= 22' # glob is not backported below 22.x

  test:
    needs: [get-matrix]
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        node-version: ${{ fromJson(needs.get-matrix.outputs.latest) }}
        os:
          - macos-latest
          - ubuntu-latest
          - windows-latest

    steps:
      - uses: actions/checkout@v4
      - name: Use node ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - name: npm clean install
        run: npm ci
      - run: node --run test

請注意,測試檔案很可能應用了不同的 tsconfig.json(因此在上面的示例中它們被排除了)。

生成型別宣告

型別宣告(.d.ts 及相關檔案)以伴隨檔案的形式提供型別資訊,允許執行程式碼是原生 JavaScript 的同時仍然擁有型別。

由於這些是根據原始碼生成的,它們可以作為釋出過程的一部分構建,無需檢入到你的程式碼倉庫中。

以下面的例子為例,型別宣告在釋出到 npm 登錄檔之前生成。

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

# This is mostly boilerplate.

name: Publish to npm
on:
  push:
    tags:
      - '**@*'

jobs:
  build:
    runs-on: ubuntu-latest

    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci

      # - name: Publish to npm
      #   run: … npm publish …

你需要釋出一個編譯後支援所有 Node.js LTS 版本的包,因為你不知道消費者將執行哪個版本;本文中的 tsconfig 支援 Node 18.x 及更高版本。

npm publish在此之前自動執行 prepacknpmnpm pack --dry-run 之前也會自動執行 prepack(這樣你就可以輕鬆檢視你將釋出的包會是什麼樣子,而無需實際釋出)。**注意**,node --run 不會這樣做。你不能在此步驟中使用 node --run,所以這個警告在這裡不適用,但可能適用於其他步驟。

實際釋出到 npm 的步驟將包含在另一篇文章中(這涉及到本文範圍之外的幾個利弊)。

分解說明

生成型別宣告是確定性的:對於相同的輸入,你每次都會得到相同的輸出。所以沒有必要將這些提交到 Git。

npm publish 會抓取命令執行時所有適用且可用的內容;因此在釋出前立即生成型別宣告意味著這些檔案是可用的,並且會被包含進去。

預設情況下,npm publish 會抓取(幾乎)所有東西(參見 包中包含的檔案)。為了讓你的已釋出包保持最小(參見關於 node_modules 的“宇宙中最重的物體”的梗),你需要從打包中排除某些檔案(如測試和測試韌體)。將這些新增到 .npmignore 中指定的排除列表中;確保列出了 !*.d.ts 這個例外,否則生成的型別宣告將不會被髮布!或者,你可以使用 package.json 的 "files" 欄位來建立一個包含列表(如果意外遺漏了一個檔案,你的包可能會對下游使用者造成破壞,所以這是一個不太安全的選擇)。