Что такое TypeScript. Обзор для JavaScript программистов
TypeScript — это JavaScript плюс синтаксис типов
Несмотря на то что приведённое описание TypeScript не является на 100% точным (есть несколько исключений), я считаю его удобным для понимания того, как он работает: TypeScript — это JavaScript плюс синтаксис типов.
Рассмотрим следующий TypeScript код:
function add(x: number, y: number): number {
return x + y;
}
Если мы хотим запустить этот код, необходимо убрать синтаксис типов и получить JavaScript, который выполняется движком JavaScript:
function add(x, y) {
return x + y;
}
Синтаксис типов используется только для проверки типов (во время редактирования и компиляции) и даёт множество проверок согласованности и лучшее автозавершение.
Способы запуска TypeScript кода
Рассмотрим следующий проект на TypeScript:
ts-app/
tsconfig.json
src/
main.ts
util.ts
util_test.ts
test/
integration_test.ts
tsconfig.json
— файл конфигурации, указывающий TypeScript, как проверять типы и компилировать код.- Остальные файлы представляют собой исходный код TypeScript.
Давайте рассмотрим способы выполнения этого кода.
Непосредственное выполнение TypeScript
Большинство серверных режимов исполнения теперь могут выполнять код TypeScript напрямую — например, Node.js, Deno и Bun. Другими словами, следующее работает в Node.js 23.6.0+:
cd ts-app/
node src/main.ts
Сборка TypeScript
При разработке веб-приложений сборка является обычной практикой — даже для проектов на чистом JavaScript: Весь код JavaScript (код приложения и код библиотек) объединяется в один файл JavaScript (иногда больше, но никогда не больше нескольких) — обычно загружаемый из HTML-файла. Это даёт несколько преимуществ:
- До появления HTTP/2 только один файл можно было обслужить одним соединением. Но это преимущество пакетирования уже не актуально.
- Каждый файл, запрашиваемый клиентом и обрабатываемый им, всё равно несёт небольшие накладные расходы (даже если не открывается новое соединение).
- Веб-серверам не приходится обслуживать множество (часто небольших) файлов, что повышает эффективность.
- Один большой файл можно сжать лучше, чем множество маленьких файлов.
Большинство бандлеров поддерживают TypeScript — либо напрямую, либо через плагины. Это означает, что мы запускаем наш TypeScript код через JavaScript файл bundle.js
, созданный бандлером:
ts-app/
tsconfig.json
src/
main.ts
util.ts
util_test.ts
test/
integration_test.ts
dist/
bundle.js
Транспиляция TypeScript в JavaScript
Другой вариант — скомпилировать приложение TypeScript в JavaScript с помощью компилятора TypeScript tsc
и запустить полученный код. До появления встроенной поддержки TypeScript в серверных средах исполнения JavaScript это был единственный способ запуска TypeScript.
Компиляция исходного кода в исходный код также называется транспиляцией. tsconfig.json
определяет, куда будет записываться вывод транспиляции. Предположим, мы запишем его в каталог dist/
:
ts-app/
tsconfig.json
src/
main.ts
util.ts
util_test.ts
test/
integration_test.ts
dist/
src/
main.js
util.js
util_test.js
test/
integration_test.js
Расширения имён файлов локально импортируемых модулей TypeScript
По умолчанию TypeScript не изменяет спецификаторы импортируемых модулей. Это означает, что локальный импорт транспилируемого кода должен выглядеть следующим образом:
// main.ts
import {helperFunc} from './util.js';
Однако можно указать TypeScript переписать расширение имени файла .ts
в .js
. Тогда следующий импорт работает как при непосредственном выполнении кода, так и при его транспонировании:
// main.ts
import {helperFunc} from './util.ts';
Публикация пакета библиотеки в npm реестре
Реестр npm по-прежнему остаётся самым популярным способом публикации пакетов. Несмотря на то что Node.js поддерживает пакеты приложений, написанные на TypeScript, пакеты библиотек должны быть развёрнуты как JavaScript код — так, чтобы они могли использоваться как JavaScript, так и TypeScript. Поэтому один файл библиотеки lib.ts
часто разворачивается в виде пяти файлов (четыре из которых компилируются TypeScript из lib.ts
):
- Основные:
lib.js
: JavaScript-частьlib.ts
lib.d.ts
: частьlib.ts
, посвящённая типам.
- Опционально: source map. Они сопоставляют места расположения исходного кода результатов компиляции с
lib.ts
.lib.js.map
: source map дляlib.js
lib.d.ts.map
: source msp дляlib.d.ts
lib.ts
: цель двух предыдущих source map
(Подробнее, что всё это значит, вы узнаете через секунду).
В качестве примера рассмотрим следующий пакет библиотеки:
ts-lib/
package.json
tsconfig.json
src/
lib.ts
dist/
src/
lib.js
lib.js.map
lib.d.ts
lib.d.ts.map
package.json
— описание пакета нашей библиотеки в npm. Некоторые из его данных, например, так называемые экспорты пакета(package exports), также используются TypeScript — например, для поиска информации о типе, когда кто-то импортирует из данного пакета.- Каждый файл в
dist/
был сгенерирован с помощью TypeScript. Обычно их не добавляют в системы контроля версий, потому что их легко перегенерировать. tsconfig.json
не загружается в реестр npm.
Основные: .js
и .d.ts
Интересно, что комбинация JavaScript и типов в lib.ts
разделяется на lib.js
, содержащий только JavaScript, и lib.d.ts
, содержащий только типы. Зачем это нужно? Это позволяет использовать пакеты библиотек как в JavaScript коде, так и в TypeScript коде:
- JavaScript код игнорирует файлы
.d.ts
. - TypeScript использует их для проверки типов, автозавершения и документирования.
На самом деле, за кулисами многие редакторы (например, Visual Studio Code) используют своего рода облегчённый режим TypeScript при редактировании JavaScript кода, что позволяет нам также получить простую проверку типов и дописывание кода.
Это исходный файл TypeScript lib.ts
/** Сложение двух чисел. */
export function add(x: number, y: number): number {
return x + y; // сложение чисел
}
Он делится на lib.js
:
/** Сложение двух чисел. */
export function add(x, y) {
return x + y; // сложение чисел
}
//# sourceMappingURL=lib.js.map
И lib.d.ts
:
/** Сложение двух чисел. */
export declare function add(x: number, y: number): number;
//# sourceMappingURL=lib.d.ts.map
Опционально: source map
Если мы компилируем файл I
в файл O
, то source map для O
сопоставляет места исходного кода в O
с местами исходного кода в I
. Это означает, что можно работать с O
, но видеть информацию из I
— например:
lib.js.map
: сопоставляет местоположения кодаlib.js
с местоположениями кодаlib.ts
и предоставляет отладку и трассировку стека для последнего при запуске первого.lib.d.ts.map
: сопоставляет строкиlib.d.ts
со строкамиlib.ts
. Включает функциюперейти к определению
для импортов изlib.ts
, чтобы перенести нас в этот файл.
Все функции, связанные с source map, за исключением трассировки стека, требуют доступа к исходному TypeScript коду. Вот почему имеет смысл включать lib.ts
, если есть source map.
Так выглядит lib.js.map
:
{
"version": 3,
"file": "lib.js",
"sourceRoot": "",
"sources": [
"../../src/lib.ts"
],
"names": [],
"mappings": "AAAA,uBAAuB;AACvB,MAAM,UAAU,···"
}
Так выглядит lib.d.ts.map
:
{
"version": 3,
"file": "lib.d.ts",
"sourceRoot": "",
"sources": [
"../../src/lib.ts"
],
"names": [],
"mappings": "AAAA,uBAAuB;AACvB,wBAAgB,GAAG,···"
}
В обоих случаях фактическое содержимое «сопоставлений» было сокращено. А в реальном выводе tsc
JSON всегда сжимается в одну строку.
DefinitelyTyped: репозиторий с типами для пакетов npm без типов
Сейчас многие пакеты npm поставляются с типами TypeScript. Однако далеко не во всех они есть. В этом случае может помочь DefinitelyTyped: Если он поддерживает пакет pkg
без типов, то можно установить пакет с типами @types/pkg
для pkg
.
Одним из важных пакетов DefinitelyTyped для Node.js является пакет @types/node
с типами для всех его API. Если вы разрабатываете TypeScript на Node.js, то этот пакет обычно входит в число зависимостей разработки.
Компиляция TypeScript с помощью инструментов, отличных от tsc
Давайте вспомним задачи, выполняемые tsc
(в этом разделе пропустим source map):
- Компилирует файлы TypeScript в файлы JavaScript.
- Компилирует файлы TypeScript в файлы объявления типов.
- Проверяет типы TypeScript файлов.
Третий пункт настолько сложен, что его может выполнить только tsc
. Однако и для первого, и второго существуют несколько более простые подмножества TypeScript, в которых компиляция не требует ничего, кроме синтаксической обработки. Это означает, что для пунктов один и два можно использовать внешние, более быстрые инструменты.
Есть также параметры tsconfig.json
, предупреждающие нас, если мы не придерживаемся этих подмножеств TypeScript (подробнее (eng))). На практике это не такая уж большая жертва.
Type Stripping
Type stripping — простейший способ компиляции TypeScript в JavaScript:
- Компиляция заключается только в удалении синтаксиса типов.
- Никакие функции языкового уровня не транспилируются.
Второй пункт означает, что некоторые возможности TypeScript не поддерживаются — например:
- JSX (HTML синтаксис внутри TypeScript, используемый в React)
- Перечисления
- Свойства параметров в конструкторах классов.
- Пространства имён
- Будущий JavaScript, скомпилированный в текущий JavaScript
Одним из существенных преимуществ type stripping является то, что оно не требует настройки (через tsconfig.json
или другими способами), потому что очень простое. Это делает платформы, использующие его, более устойчивыми к изменениям, вносимым в TypeScript.
Техника type stripping: замена типов пробелами
Одна из умных техник удаления типов была впервые применена в инструменте ts-blank-space
(автор Ashley Claymore для Bloomberg): Вместо удаления синтаксиса типа он заменяет его пробелами. Это означает, что позиции исходного кода в выводе не меняются. Таким образом, все позиции, отображаемые (например, в трассировках стека), продолжают работать на входе, и необходимость в source map снижается: Они по-прежнему нужны для отладки и перехода к определениям, но JavaScript, сгенерированный в результате type stripping, относительно близок к исходному TypeScript, и часто даже в этом случае всё в порядке.
Например — ввод (TypeScript):
function add(x: number, y: number): number {
return x + y;
}
Вывод (JavaScript):
function add(x , y ) {
return x + y;
}
Если хотите исследовать глубже, можете заглянуть на игровую площадку ts-blank-space
.
Изолированные объявления
«Изолированное объявление» — стиль написания TypeScript, при котором типы для файла объявления (.d.ts
) легко извлекаются. В основном это означает предоставление возвращаемых типов для экспортируемых функций. В принципе, TypeScript может сделать это за нас, но простые генераторы файлов объявлений этого сделать не могут. Это ограничение не существует для неэкспортируемых функций, потому что они не отображаются в файлах объявления.
Первая версия файла TypeScript strings.ts
:
// Не OK: экспортируемая функция без возвращаемого типа
export function upperCase(str: string) {
return str.toUpperCase();
}
// Не экспортируется, возвращаемый тип не требуется
function internalHelper() {}
strings.ts
в стиле изолированных объявлений:
// OK: экспортируемая функция содержит возвращаемый тип
export function upperCase(str: string): string {
return str.toUpperCase();
}
// Не экспортируется, возвращаемый тип не требуется
function internalHelper() {}
Это сгенерированный файл объявлений strings.d.ts
(обратите внимание, что internalHelper
в нем отсутствует):
export declare function upperCase(str: string): string;
JSR — JavaScript реестр
JavaScript реестр JSR — альтернатива npm и npm реестру для публикации пакетов. Он работает следующим образом:
- Для пакетов TypeScript загружаются только
.ts
файлы. - Способ установки пакета TypeScript зависит от платформы:
- На платформах JavaScript, поддерживающих только пакеты библиотек TypeScript, JSR устанавливает только TypeScript.
- На всех остальных платформах JSR автоматически генерирует
.js
файлы и.d.ts
файлы и устанавливает их вместе с.ts
файлами. Чтобы автоматическая генерация была возможна, код TypeScript должен следовать набору правил, называемыхбез медленных типов
— это похоже на изолированные объявления.
В отличие от npm реестра, ваш пакет TypeScript библиотеки можно использовать на Node.js только в том случае, если вы загрузите .js
файлы и .d.ts
файлы.
JSR также предоставляет несколько возможностей, которых нет у npm, например, автоматическую генерацию документации. Дополнительную информацию можно найти в официальной документации Why JSR?.
Кому принадлежит JSR
Цитирую страницу официальной документации Governance:
JSR не принадлежит ни одному человеку или организации. Это проект, управляемый сообществом, открытый для всех и созданный для всей экосистемы JavaScript.
В настоящее время JSR управляется компанией Deno. В настоящее время мы работаем над созданием совета по управлению проектом, который затем будет работать над переводом проекта в фонд.
Редактирование TypeScript
Две популярные IDE для JavaScript:
- Visual Studio Code (бесплатно)
- WebStorm (платно)
Замечания в этом разделе относятся к Visual Studio Code, но могут быть применимы и к другим IDE.
В Visual Studio Code мы получаем два различных способа проверки типов:
- Любой открытый в данный момент файл автоматически проверяется на соответствие типу в Visual Studio Code. Чтобы обеспечить эту функциональность, она поставляется с собственной установкой TypeScript.
- Если мы хотим проверить тип всей кодовой базы, необходимо вызвать компилятор TypeScript
tsc
. Это можно сделать с помощью задач Visual Studio Code — встроенного способа вызова внешних инструментов (для проверки типов, компиляции, пакетирования и т. д.). Более подробная информация о задачах содержится в официальной документации.
Проверка типов JavaScript файлов
Опционально TypeScript может проверять типы JavaScript файлов. Очевидно, что это даст лишь ограниченные результаты. Однако чтобы помочь TypeScript, можно добавить информацию о типе через комментарии JSDoc — например:
/**
* @param {number} x - Первый операнд
* @param {number} y - Второй операнд
* @returns {number} Сумма обоих операндов
*/
function add(x, y) {
return x + y;
}
Если сделать так, то мы все равно будем писать на TypeScript, просто с другим синтаксисом.
Преимущества такого подхода:
- Нет необходимости в шаге сборки для запуска кода — даже на платформах (например, в браузерах), не поддерживающих TypeScript.
- Кроме того, можно генерировать
.d.ts
файлы из.js
файлов с комментариями JSDoc. Однако это дополнительный шаг сборки. О том, как это сделать, рассказывается в TypeScript Handbook Creating .d.ts Files from .js files.
- Кроме того, можно генерировать
- Это позволяет сделать кодовую базу JavaScript более типобезопасной — небольшими постепенными шагами.
Недостатки такого подхода:
- Синтаксис становится менее приятным в использовании.
Чтобы пояснить недостатки, рассмотрим, определение интерфейса в TypeScript:
interface Point {
x: number;
y: number;
/** опциональное свойство */
z?: number;
}
И через JSDoc:
/**
* @typedef Point
* @prop {number} x
* @prop {number} y
* @prop {number} [z] опциональное свойство
*/
Более подробная информация содержится в TypeScript Handbook: