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固有の構文を削除し、Nodeがすでに理解できる純粋なJavaScriptを残します。この動作はNodeバージョン22.18.0以降、デフォルトで有効になっています。

    • Nodeは、node_modules内の型をストリッピングしません。これは公式のTypeScriptコンパイラ(tsc)やVS Codeの一部で重大なパフォーマンス問題を引き起こす可能性があるため、TypeScriptのメンテナーは、少なくとも現時点では、生のTypeScriptを公開しないように推奨しています。
  • NodeでenumのようなTypeScript固有の機能を使用するには、依然としてフラグ(--experimental-transform-types)が必要です。これらについては、より良い代替手段が存在することが多いです。

    • TypeScript固有の機能が存在しないこと(コードがNodeでそのまま実行できること)を保証するには、TypeScriptバージョン5.8以降でerasableSyntaxOnly設定オプションを設定します。
  • GitHub Actions内のものを含め、依存関係を最新に保つためにDependabotを使用してください。これは設定して放置できる非常に簡単な設定です。

  • .nvmrcは、Nodeのマルチバージョンマネージャであるnvmに由来します。これにより、プロジェクトが一般的に使用すべき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は、mainブランチへのPRに対して、型の検査がパスすることを自動的にチェック(および要求)するCIタスクを設定します。

# 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を実行しているかわからないため、すべてのNode.js LTSバージョンをサポートするようにコンパイルされたパッケージを公開する必要があります。この記事のtsconfigはNode 18.x以降をサポートしています。

npm publish事前にprepackを自動的に実行します。npmnpm pack --dry-runの前にも自動的にprepackを実行します(そのため、実際に公開することなく公開されるパッケージを簡単に確認できます)。注意node --runはそのようなことはしません。このステップではnode --runを使用できないため、その注意点はここでは適用されませんが、他のステップでは適用される可能性があります。

実際にnpmに公開する手順は別の記事で説明します(この記事の範囲を超えるいくつかの長所と短所があります)。

これを分解すると

型定義の生成は決定論的です。つまり、同じ入力から毎回同じ出力が得られます。したがって、これらをgitにコミットする必要はありません。

npm publishは、コマンドが実行された瞬間に適用可能で利用可能なすべてを取得します。したがって、直前に型定義を生成すると、それらが利用可能になり、取り込まれます。

デフォルトでは、npm publishは(ほぼ)すべてを取得します(パッケージに含まれるファイルを参照)。公開するパッケージを最小限に抑えるため(node_modulesに関する「宇宙で最も重い物体」のミームを参照)、特定のファイル(テストやテストフィクスチャなど)をパッケージングから除外したい場合があります。これらを.npmignoreで指定されたオプトアウトリストに追加します。!*.d.tsの例外がリストされていることを確認してください。さもないと、生成された型定義が公開されません!あるいは、package.jsonの"files"を使用してオプトインを作成することもできます(誤ってファイルを除外し忘れると、下流のユーザーにとってパッケージが壊れる可能性があるため、これはより安全でないオプションです)。