Представляю вашему вниманию перевод статьи "Working With TypeScript: A Practical Guide for Developers".
Что такое TypeScript?
TypeScript — это популярный статический типизатор (static type checker) или типизированное надмножество (typed superset) для JavaScript, инструмент, разработанный Microsoft и добавляющий систему типов к гибкости и динамическим возможностям JavaScript.
TypeScript развивается как проект с открытым исходным кодом, распространяется под лицензией Apache 2.0, имеет очень активное и высокопрофессиональное сообщество, а также огромное влияние на экосистему JavaScript.
Установка TypeScript
Для того, чтобы начать работу с TypeScript, нужно либо установить специальный интерфейс командной строки (command line interface, CLI), либо воспользоваться официальной онлайн-песочницей или другим похожим инструментом.
Для выполнения кода мы будем использовать Node.js. Устанавливаем его, если он еще не установлен на вашей машине, инициализируем новый Node.js-проект и устанавливаем транспилятор TypeScript:
# Создаем новую директорию для проекта
mkdir typescript-intro
# Делаем созданную директорию текущей
cd typescript-intro
# Инициализируем Node.js-проект
npm init -y
# Устанавливаем компилятор TypeScript
npm i typescript
Это установит tsc
(компилятор TypeScript) для текущего проекта. Для того, чтобы проверить установку, в директории проекта создаем файл index.ts
следующего содержания:
console.log(1)
Затем используем транспилятор для преобразования кода, содержащегося в этом файле, в JavaScript:
# Преобразуем index.ts в index.js
npx tsc index.ts
Наконец, выполняем скомпилированный код с помощью команды node
:
# Вы должны увидеть `1` в терминале
node index.js
В данном случае транспилятор не делает ничего, кроме копирования кода из одного файла в другой, но это позволяет убедиться, что все установлено и работает правильно.
Обратите внимание: версии TypeScript могут сильно отличаться друг от друга, даже если речь идет о минорных релизах. Поэтому TypeScript лучше устанавливать локально и выполнять с помощью npx
, вместо того, чтобы полагаться на глобальную версию.
Определение TypeScript-проекта
Для определения TypeScript-проекта внутри Node.js-проекта, необходимо создать файл tsconfig.json
. Присутствие данного файла в директории свидетельствует о том, что мы имеем дело с TypeScript-проектом.
tsconfig.json
содержит определенное количество настроек, которые влияют на поведение транспилятора, например, на то, какие файлы следует игнорировать, какой файл является целью компиляции, какие типы импортируются и т.д.
Вы легко можете настроить TypeScript с помощью следующей команды:
# Создаем стандартный tsconfig.json
npx tsc --init
Сгенерированный tsconfig.json
содержит почти все возможные настройки с кратким описанием каждой из них. К счастью, данный файл содержит хорошие настройки по умолчанию, так что вы можете удалить большую часть закомментированных опций.
Мы еще вернемся к настройкам TypeScript, а сейчас давайте писать код.
Возможности TypeScript
Каждая возможность TypeScript подробно рассматривается в "Карманной книге по TypeScript". Мы сосредоточимся на практической составляющей некоторых из них. Я постараюсь пролить свет на некоторые возможности, которые часто упускаются из вида в литературе, посвященной TypeScript.
Основы типизации
Ключевая идея TypeScript заключается в контроле за динамической природой и гибкостью JavaScript с помощью типов. Давайте рассмотрим эту идею на практике.
В директории проекта создаем файл test.js
следующего содержания:
function addOne(age) {
return age + 1
}
const age = 'thirty two'
console.log(addOne(age))
Выполняем данный код:
node test.js
- Что мы увидим в терминале?
- Как вы думаете, правильным ли будет вывод?
В терминале мы увидим thirty two1
без каких-либо предупреждений об очевидной некорректности вывода. Ничего нового: обычное поведение JavaScript.
Но что если мы хотим обеспечить, чтобы функция addOne()
принимала только числа? Вы можете добавить в код проверку типа переданного значения с помощью оператора typeof
или же вы можете использовать TypeScript, который привнесет в процесс компиляции кода некоторые ограничения.
Заменим содержимое созданного нами ранее index.ts
следующим кодом:
function addOne(age: number): number {
return age + 1
}
console.log(addOne(32))
console.log(addOne('thirty two'))
Обратите внимание, что мы ограничили принимаемый функцией аргумент и возвращаемое функцией значение типом number
.
Преобразуем файл:
npx tsc index.ts
Попытка преобразования проваливается:
index.ts:6:20 - error TS2345: Argument of type 'string' is not
assignable to parameter of type 'number'. Аргумент типа 'строка' не может быть присвоен параметру с типом 'число'.
Таким образом, определение типов помогает избежать совершения ошибок, таких как передача в функцию аругментов неправильного типа.
string
и number
— это лишь два из основных типов, поддерживаемых TypeScript. TypeScript поддерживает все примитивные значения JavaScript, включая boolean
и symbol
.
Кроме того, TypeScript определяет несколько собственных типов, которые не имеют соответствия в JavaScript, но являются очень полезными с точки зрения используемой в данной экосистеме методологии:
enum
— ограниченный набор значенийany
— указывает на то, что переменная/параметр могут быть чем угодно, что, по сути, нивелирует типизациюunknown
— типобезопасная альтернативаany
void
— указывает на то, что функция ничего не возвращаетnever
— указывает на то, что функция выбрасывает исключение или на то, что ее выполнение никогда не заканчивается- литеральные типы, конкретизирующие типы
number
,string
илиboolean
. Это означает, например, что 'Hello World' — этоstring
, ноstring
— это не 'Hello World' в контексте системы типов. Тоже самое справедливо в отношенииfalse
в случае с логическими значениями или для3
в случае с числами:
// Данная функция принимает не любое число, а только 3 или 4
declare function processNumber(s: 3 | 4)
declare function processAnyNumber(n: number)
const n: number = 10
const n2: 3 = 3
processNumber(n) // Ошибка: `number` - это не `3 | 4`
processAnyNumber(n2) // Работает. 3 - это `number`
Множества
TypeScript поддерживает несколько типов множеств (обычные массивы, ассоциативные массивы — карты или мапы, кортежи), обеспечивая первоклассную поддержку композиции.
Карты (maps)
Карты, как правило, используются для определения связи между ключами и значениями для представления специфичных для приложения данных:
// Создаем ассоциативный тип
type User = {
id: number
username: string
name: string
}
// Создаем объект `user`, соответствующий ассоциативному типу
const user: User = {
id: 1,
username: 'Superman',
name: 'Clark Kent',
}
Векторы (vectors)
Векторы — это последовательная индексированная структура данных, содержащая фиксированные типы для всех элементов. JavaScript не поддерживает данную возможность, но TypeScript позволяет разработчикам эмулировать эту концепцию:
// Создаем ассоциативный тип
type User = {
id: number
username: string
name: string
}
// Создаем несколько объектов `user`, соответствующих ассоциативному типу
const user1: User = {
id: 1,
username: 'Superman',
name: 'Clark Kent',
}
const user2: User = {
id: 2,
username: 'WonderWoman',
name: 'Diana Prince',
}
const user3: User = {
id: 3,
username: 'Spiderman',
name: 'Peter Parker',
}
// Создаем вектор пользователей
const userVector: User[] = [user1, user2, user3]
Кортежи (tuples)
Кортежи также являются индексированной структорой данных, но типы элементов могут различаться в зависимости от их позиции:
// Создаем ассоциативный тип
type User = {
id: number
username: string
name: string
}
// Создаем объект `user`, соответствующий ассоциативному типу
const user1: User = {
id: 1,
username: 'Superman',
name: 'Clark Kent',
}
// Создаем кортеж
const userTuple: [User, number] = [user1, 10]
Объединения (unions)
Другим способом создания композиции типов являются объединения, которые бывают полезны в случаях, когда принимаемый функцией аргумент может иметь один из нескольких типов.
Предположим, что мы хотим создать функцию, возвращающую адрес пользователя на основе переданного объекта или строки.
Прежде всего, давайте установим node-fetch
, чтобы иметь возможность использовать функцию fetch
в Node.js:
npm i node-fetch @types/node-fetch
Затем с помощью typeof
осуществляем разделение типов:
type User = {
id: number
username: string
name: string
email: string
}
async function fetchFromEmail(email: string) {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const parsed: User[] = await res.json()
const user = parsed.find((u: User) => u.email === email)
if (user) {
return fetchFromId(user.id)
}
return undefined
}
function fetchFromId(id: number) {
return fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((res) => res.json())
.then((user) => user.address)
}
function getUserAddress(user: User | string) {
if (typeof user === 'string') {
return fetchFromEmail(user)
}
return fetchFromId(user.id)
}
getUserAddress('Rey.Padberg@karina.biz').then(console.log).catch(console.error)
Здесь мы в явном виде реализовали предохранитель типов.
К слову, кортежи и объединения можно использовать совместно:
const userTuple: Array<User | number> = [u, 10, 20, u, 30]
// Любой элемент может быть либо `User`, либо `number`
Можно определять размер и тип каждого элемента массива:
const userTuple: [User, number] = [u, 10, 20, u, 30]
// Ошибка: массив должен состоять из двух элементов с типами `User` и `number`
const anotherUserTuple: [User, number] = [u, 10] // Все верно
Предохранители типов (type guards)
Предохранители типов — это выражения, выполняющие проверки во время выполнения кода, результат которых может быть использован системой типов для сужения (narrow) области (scope) проверяемого аргумента.
Одним из предохранителей является оператор typeof
, который мы использовали в предыдущем примере для сужения области аргумента user
.
Существуют и другие предохранители, такие как instanceof
, !==
и in
, полный список можно найти в документации.
Для случаев, когда система типов не может сделать правильный вывод о типе в текущем контексте, мы можем определить пользовательский предохранитель типа через предикат (типизированная функция, возвращающая логическое значение):
// Определяем предохранитель для `user`
function isUser(u: unknown): u is User {
if (u && typeof u === 'object') {
return 'username' in u && 'currentToken' in u
}
return false
}
function getUserAddress(user: User | string) {
if (isUser(user)) {
return fetchFromEmail(user)
}
return fetchFromId(user.id)
}
Пользовательские предохранители находятся под полным контролем разработчика, TypeScript не имеет возможности убедиться в их корректности.
Весьма распространенным случаем использования пользовательских предохранителей является влидация внешних данных с помощью JSON-схемы, предоставляемой сторонней библиотекой, такой как Ajv. Обычно, это происходит в веб-приложениях, где тело запроса имеет тип unknown
(или any
в зависимости от используемого фреймворка), и мы хотим проверить его перед использованием:
import Ajv from 'ajv'
const ajv = new Ajv()
const validate = ajv.compile({
type: 'object',
properties: {
username: { type: 'string' },
currentToken: { type: 'string' },
},
})
function validateUser(data: unknown): data is User {
return validate(data)
}
В основе данного механизма лежит синхронизация JSON-схемы с типом. Если мы изменим тип, но не изменим схему, то вполне можем получить неожиданное сужение типа.
В следующем разделе мы узнаем, как обеспечить автоматическую синхронизацию между схемой и типом.
Исключающие объединения (discriminated unions)
Объединения с общим литеральным полем называются исключающими. При работе с такими типами TypeScript предоставляет неявный предохраитель, позволяя избежать его создания в явном виде:
type Member = {
type: 'member'
currentProject: string
}
type Admin = {
type: 'admin'
projects: string[]
}
type User = Member | Admin
function getFirstProject(u: User) {
if (u.type === 'member') {
return u.currentProject
}
return u.projects[0]
}
В функции getFirstProject()
TypeScript сужает область аргумента без помощи предиката. Попытка получить доступ к массиву projects
в первой ветке (блоке if
) закончится ошибкой типа.
Валидация во время выполнения
Как было отмечено ранее, в случае с пользовательскими предохранителями, корректность возвращаемого результата обеспечивается разработчиком.
При наличии ошибки в предикате, система типов может получить неверную информацию. Рассмотрим пример:
function validateUser(data: unknown): data is User {
return true
}
Данный предикат всегда возвращает true
, позволяя типизатору сузить тип к тому, чем он на самом деле не является:
const invalidUser = undefined
if (validateUser(invalidUser)) {
// Предыдущая инструкция всегда возвращает `true`
console.log(invalidUser.name) // Ошибка, возникающая во время выполнения
}
Существует несколько библиотек, которые позволяют обеспечить автоматическую синхронизацию между валидацией во время выполнения и соответствующим типом. Одним из самых популярных решений является runtypes, однако мы будем использовать io-ts и fp-ts.
Суть данного подхода состоит в том, что мы определяем форму (или фигуру) типа с помощью примитивов, предоставляемых io-ts
; эта форма называется декодером (decoder); мы используем ее для проверки данных, которым мы по какой-либо причине не доверяем:
import * as D from 'io-ts/Decoder';
import * as E from 'io-ts/Either';
import { pipe } from 'fp-ts/function';
// Определяем декодер, представляющий `user`
const UserDecoder = D.type({
id: D.number,
username: D.string,
name: D.string
email: D.string
});
// и используем его в отношении потенциально опасных данных
pipe(
UserDecoder.decode(data),
E.fold(
error => console.log(D.draw(error)),
decodedData => {
// типом `decodedData` является `User`
console.log(decodedData.username)
}
)
);
Настройка TypeScript
Поведение транспилятора можно настраивать с помощью файла tsconfig.json
, находящегося в корне проекта.
Данный файл содержит набор ключей и значений, отвечающих за 3 вещи:
- Структура проекта: какие файлы включаются/исключаются из процесса компиляции, зависимости разных TypeScript-проектов, связь между этими проектами через синонимы (aliases).
- Поведение типизатора: выполнять ли проверку на наличие
null
иundefined
в кодовой базе, сохранениеconst enums
и т.п. - Процесс транспиляции.
Пресеты TSConfig
TypeScript может преобразовывать код в ES3
и поддерживает несколько форматов модулей (CommonJS, SystemJS и др.).
Точные настройки зависят от среды выполнения кода. Например, если вашей целью является Node.js 10, вы можете транспилировать код в ES2015
и использовать CommonJS
в качестве стратегии разрешения модулей.
Если вы используете последнюю версию Node.js, например, 14 или 15, тогда можете указать в качестве цели ESNext
или ES2020
и использовать модульную стратегию ESNext
.
Наконец, если вашей целью является браузер и вы не используете сборщик модулей, такой как webpack
или parcel
, то можете использовать UMD
.
К счастью, команда TypeScript разработала хороший набор пресетов, которые вы можете просто импортировать в свой tsconfig.json
:
{
"extends": "@tsconfig/node12/tsconfig.json",
"include": ["src"]
}
Среди наиболее важных настроек, можно отметить следующее:
declaration
: определяет, должен ли TypeScript генерировать файлы определений (.d.ts
) во время транспиляции. Данные файлы, как правило, используются при разработке библиотекnoEmitOnError
: определяет, должен ли TypeScript прерывать процесс компиляции при возникновении ошибок, связанных с неправильными типами. Рекомендуемым значением данной нстройки являетсяtrue
removeComments
:true
suppressImplicitAnyIndexErrors
:true
strict
: дополнительные проверки. До тех пор, пока у вас не появится веской причины для отключения данной настройки, она должна иметь значениеtrue
noEmitHelpers
: при необходимости, TypeScript предоставляет утилиты и полифилы для поддержки возможностей, которых не было вES3
иES5
. Если значение данной настройки являетсяfalse
, утилиты будут помещены в начало кода, в противном случае, они будут опущены (tslib
можно устанавливать отдельно)
Заключение
Надеюсь, данная статья позволила вам получить общее предствления о возможностях, предоставляемых TypeScript, а также о том, почему использование TypeScript в дополнение к JavaScript в настоящее время фактически является стандартом веб-разработки.
Система типов TypeScript не является идеальной, но это лучшее, что мы имеет на сегодняшний день.
Облачные серверы от Маклауд отлично подходят для сайтов с JavaScript.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!