Первый проект TypeScript
TypeScript помогает согласовать разные части программы до её запуска. В учебном каталоге мы ожидаем, что число уроков является числом, название — строкой, а функция сводки возвращает предсказуемый результат. Пока проект помещается в один файл, эти договорённости легко помнить. При переносе модели в отдельный модуль они начинают зависеть от внимательности каждого автора.
В этой серии возьмём четыре курса из учебного каталога JavaScript и опишем его договорённости средствами типов. Начнём с устройства проекта: где находится исходный TypeScript, кто проверяет его и какой файл получает браузер. Мы не будем заново изучать циклы и массивы. Нужны JavaScript, простые объекты и понимание подключения ES-модулей. Исходники этой партии подготовлены чтением; компилятор и учебные программы при подготовке не запускались, поэтому показанные результаты являются ожидаемыми.
Исходник, проверка и выполнение
У нашего проекта три разных действия. Вы редактируете src/app.ts, инструмент tsc читает исходники и сопоставляет их типы, а браузер выполняет полученный dist/app.js. Ошибка на втором этапе может остановить получение нового JavaScript, хотя браузер ничего не знает о причине остановки. Поэтому надпись в браузере и успешная проверка исходников — разные свидетельства.
Выберем опубликованную версию TypeScript 6.0.2 и закрепим её явно. Это версия примеров курса, а не обещание использовать самый новый выпуск. Номер и исполняемая команда tsc указаны в package.json официального тега Microsoft. Создайте каталог type-lab, а в нём полный package.json:
{
"name": "professorweb-type-lab",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"build": "tsc"
},
"devDependencies": {
"typescript": "6.0.2"
}
}
Поле private предотвращает случайную публикацию учебного пакета в npm. Поле type сообщает инструментам, что JavaScript-файлы проекта относятся к ES-модулям. Две команды различаются намеренно: check только проверяет исходники, build ещё записывает результат. Никакой сервер и никакой браузер эти команды сами не открывают. Для их выполнения читателю нужны Node.js и npm; устанавливать библиотеки в браузер не требуется.
Добавьте полный tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"lib": [
"ES2022",
"DOM"
],
"types": [],
"rootDir": "src",
"outDir": "dist",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmitOnError": true,
"verbatimModuleSyntax": true
},
"include": [
"src/**/*.ts"
]
}
В include перечислена область исходников. rootDir и outDir задают соответствие src/app.ts и dist/app.js. Мы фиксируем target и формат модулей, чтобы результат не зависел от изменяющихся значений по умолчанию. lib описывает доступные встроенные возможности ES2022 и браузера. Это описание не устанавливает полифиллы и не гарантирует наличие каждого API у посетителя.
Режим strict включает строгую проверку типов. Две дополнительные настройки понадобятся позже: доступ к неизвестному индексу будет учитывать отсутствие элемента, а необязательное поле будет отличаться от поля с явно записанным undefined. types: [] ограничивает подключение внешних глобальных объявлений: наш браузерный пример не использует серверный process. Значение moduleResolution: Bundler здесь задаёт правила поиска модулей инструментом, но не добавляет сборщик. Все импорты локальные, готовые файлы браузер загружает непосредственно.
Четыре курса и первая аннотация
В src/data.ts сохраните четыре объекта. Их стабильные id — javascript, html-css, performance, markdown; числа уроков — 20, 16, 12 и 12. Первые три курса относятся к frontend, последний — к publishing. Это фиксированные учебные данные, а не текущая статистика сайта. Полный файл включён в архив исходников серии, в папке lesson-01. При самостоятельном создании используйте такую полную запись:
export const courses = [
{
"id": "javascript",
"title": "Современный JavaScript",
"topic": "frontend",
"lessons": 20,
"url": "./courses/javascript.html"
},
{
"id": "html-css",
"title": "HTML и CSS",
"topic": "frontend",
"lessons": 16,
"url": "./courses/html-css.html"
},
{
"id": "performance",
"title": "Производительность сайта",
"topic": "frontend",
"lessons": 12,
"url": "./courses/performance.html"
},
{
"id": "markdown",
"title": "Статический сайт из Markdown",
"topic": "publishing",
"lessons": 12,
"url": "./courses/markdown.html"
}
];
Полный src/app.ts вычисляет сводку:
import { courses } from './data.js';
const total: number = courses.reduce((sum, course) => sum + course.lessons, 0);
console.log(`Курсов: ${courses.length}. Уроков: ${total}.`);
После имени total записано : number. Такая аннотация задаёт ограничение для значения переменной. Она не вызывает преобразование и не меняет арифметику reduce. Сумму по-прежнему вычисляет JavaScript. Если заменить числовое поле одного исходного объекта строкой, проблема станет видна в отношениях типов ещё до обращения к браузеру. Конкретный текст диагностического сообщения зависит от версии инструмента и здесь не приводится как полученный результат.
Относительный импорт заканчивается на .js, хотя исходный файл называется data.ts. Мы пишем адрес будущего браузерного модуля. При обработке проекта TypeScript находит соответствующий исходник, а в готовом JavaScript сохраняет адрес ./data.js. Расширение .ts в браузерной ссылке без отдельной стратегии обработки исходников создало бы другую задачу.
Добавьте index.html, который подключает ./dist/app.js как script type="module". Полный документ также есть в снимке первого урока. На странице достаточно заголовка и пояснения, что результат появляется в консоли. Представление карточек введём отдельно: сейчас оно мешало бы увидеть границу между проверкой и выполнением.
Что должно получиться у читателя
Из корня своего проекта читатель последовательно устанавливает зависимость, проверяет типы и создаёт JavaScript:
npm install
npm run check
npm run build
Ожидаемая сводка при открытии страницы через HTTP:
Курсов: 4. Уроков: 60.
Для просмотра подойдёт локальный HTTP-сервер; пример команды находится в README. Открытие через file:// может дать другую политику загрузки модулей. Отсутствие сообщения в консоли ещё не доказывает ошибку типов: сначала следует посмотреть, был ли вообще загружен dist/app.js и его импорт.
Как разобраться с первой неудачной сборкой
Предположим, читатель открыл HTML раньше, чем создал JavaScript. Браузер запросит dist/app.js, которого пока нет. Это не доказательство, что TypeScript не работает: исходник существует, но этап получения исполняемого файла пропущен. Сначала нужно различить наличие исходного файла, успешное завершение команды и доступность результата по адресу подключения.
Другой случай — изменение числа уроков в data.ts после успешной сборки. Страница не читает этот исходник непосредственно, поэтому её перезагрузка без нового получения JavaScript может оставить сумму 60. Это ожидаемая граница проекта, а не особенность подсчёта. Если читатель изменил число 20 на 24 и обновил результат, ожидается сумма 64. После такого опыта верните 20, чтобы следующие главы продолжались на согласованных данных.
Мы не включаем в архив выдуманный package-lock.json: его содержание зависит от реально выполненной установки. Читатель создаёт lockfile сам и может сохранить его вместе с проектом. Точное указание TypeScript в package.json фиксирует основную учебную зависимость; lockfile фиксирует полный результат установки инструментом. Эти файлы имеют разные назначения.
Также не копируйте в src/ готовый JavaScript из чужой папки dist/, пытаясь исправить пропущенную сборку. Тогда появятся два похожих источника изменений, и будет неясно, какой из них автор редактирует. Достаточно одной цепочки: проверенный исходник, новый результат, его загрузка браузером. Порядок полезно установить до появления сложных модулей: впоследствии тот же договор позволит отделять проблему конфигурации от проблемы модели данных.
Обратите внимание на noEmitOnError. При обнаружении ошибки инструмент не должен записывать новый результат. Однако уже существующий dist/ от прежнего успешного шага автоматически не исчезает. Браузер может продолжать показывать старые данные, поэтому смотрите на завершение команды и время обновления файлов, а не только на вид страницы.
Аннотации типов стираются при создании JavaScript; это описано в вводном разделе Handbook. Посетитель получает вычисление суммы, но не описание number. Следовательно, внешнему JSON в будущих уроках понадобится настоящая проверка значений. Первый результат курса — понятное разделение ответственности: исходник хранит контракт, инструмент сопоставляет его части, среда выполняет JavaScript. В следующем уроке разберём, где контракт выводится автоматически и где его полезно записать вручную.