Эта статья охватывает моменты, касающиеся именно публикации TypeScript. Под публикацией понимается распространение в виде пакета через npm (или другой менеджер пакетов); это не про компиляцию приложения/сервера для запуска в production (например, PWA и/или endpoint-сервер).
Несколько важных моментов:
-
Всё из статьи Публикация пакета применимо и здесь.
-
Поля вроде
mainоперируют опубликованным содержимым, поэтому, когда исходный код на TypeScript транспилируется в JavaScript, опубликованным содержимым является JavaScript, иmainуказывал бы на JavaScript-файл с расширением JavaScript-файла (например,main.ts→"main": "main.js"). -
Поля вроде
scripts.testоперируют исходным кодом, поэтому они использовали бы расширения файлов исходного кода (например,"test": "node --test './src/**/*.test.ts').
-
-
Node выполняет код на TypeScript через процесс под названием «отбрасывание типов (type stripping)», в котором node (через Amaro) удаляет специфичный для TypeScript синтаксис, оставляя обычный JavaScript (который node уже понимает). Это поведение включено по умолчанию начиная с node версии 22.18.0.
- Node не отбрасывает типы в
node_modules, потому что это может вызвать значительные проблемы с производительностью официального компилятора TypeScript (tsc) и частей VS Code, поэтому сопровождающие TypeScript хотели бы отговорить людей от публикации сырого TypeScript, по крайней мере пока.
- Node не отбрасывает типы в
-
Использование специфичных для TypeScript возможностей вроде
enumв node всё ещё требует флага (--experimental-transform-types). Для них так или иначе часто есть лучшие альтернативы.- Чтобы гарантировать, что специфичных для TypeScript возможностей нет (и ваш код мог просто работать в node), установите опцию конфигурации
erasableSyntaxOnlyв TypeScript версии 5.8+.
- Чтобы гарантировать, что специфичных для TypeScript возможностей нет (и ваш код мог просто работать в node), установите опцию конфигурации
-
Используйте 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Замечание об организации директорий: есть несколько распространённых практик размещения тестов. Принцип наименьшего знания (principle of least knowledge) говорит располагать их рядом (co-locate) — рядом с реализацией. Иногда это в той же директории, иногда — в «ящичке» вроде __test__ (тоже рядом с реализацией, «Files co-located but segregated»). Как вариант, некоторые предпочитают создавать test/ рядом с src/ («'src' and 'test' fully segregated») — либо с зеркальной структурой, либо как «свалку» (junk drawer).
Назначение типов — предупредить, что реализация не будет работать:
// @errors: 2322
const foo = 'a';
const bar: number = 1 + foo;TypeScript предупредил, что код выше не будет вести себя как задумано, — точно так же, как модульный тест предупреждает, что код ведёт себя не так, как задумано. Они дополняют друг друга и проверяют разные вещи — у вас должно быть и то, и другое.
Ваш редактор (например, VS Code), скорее всего, имеет встроенную поддержку TypeScript, отображая ошибки по ходу работы. Если нет и/или вы их пропустили, вас подстрахует CI.
Следующий GitHub Action настраивает CI-задачу, которая автоматически проверяет (и требует), чтобы типы прошли инспекцию для PR в ветку main.
# 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 и компания) предоставляют информацию о типах в виде прилагаемого (sidecar) файла, позволяя исполняемому коду оставаться обычным 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 …Вам стоит публиковать пакет, скомпилированный с поддержкой всех LTS-версий Node.js, поскольку вы не знаете, на какой версии будет работать потребитель; tsconfig в этой статье поддерживают node 18.x и новее.
npm publish автоматически запускает prepack заранее. npm также автоматически запускает prepack перед npm pack --dry-run (чтобы вы могли легко увидеть, каким будет ваш опубликованный пакет, фактически не публикуя его). Осторожно, node --run этого не делает. Для этого шага нельзя использовать node --run, так что здесь эта оговорка неприменима, но она может касаться других шагов.
Шаги для собственно публикации в npm будут включены в отдельную статью (есть несколько плюсов и минусов за рамками этой статьи).
Генерация объявлений типов детерминирована: из одного и того же ввода вы каждый раз получаете один и тот же вывод. Поэтому нет нужды коммитить их в git.
npm publish захватывает всё применимое и доступное в момент запуска команды; поэтому генерация объявлений типов непосредственно перед этим означает, что они доступны и будут подхвачены.
По умолчанию npm publish захватывает (почти) всё (см. Files included in package). Чтобы держать ваш опубликованный пакет минимальным (вспомните мем «Heaviest Objects in the Universe» про node_modules), вам нужно исключить определённые файлы (например, тесты и тестовые фикстуры) из упаковки. Добавьте их в opt-out-список, указанный в .npmignore; убедитесь, что исключение !*.d.ts присутствует, иначе сгенерированные объявления типов не будут опубликованы! Как вариант, можно использовать package.json «files», чтобы создать opt-in (если случайно ошибиться и пропустить файл, ваш пакет может оказаться сломанным для нижестоящих пользователей, так что это менее безопасный вариант).