В наше время, чтобы быть конкурентоспособным в области фронтенда/Node.js, не обойтись без изучения TypeScript.
JavaScript — полезный навык, но как только ваши проекты начнут усложняться, неизбежно возникнут проблемы с сохранением четкости кода. Рассмотрим пример:
const userColorMap = new Map({
'123': ['red', 'blue', 'green'],
'456': ['yellow', 'purple', 'orange'],
'789': ['pink', 'black', 'white'],
});
const userFavouriteColors = (userId) => {
return userColorMap.get(userId);
}
Кажется, все довольно просто. Предположим, что эта функция используется следующим образом:
const favouriteColors = userFavouriteColors('123');
// Добавление нового цвета в список любимых цветов пользователя
favouriteColors.push('brown');
Пока все выглядит неплохо. Но что, если сделать так:
const favouriteColors = userFavouriteColors('000');
// Добавление нового цвета в список любимых цветов пользователя
favouriteColors.push('brown');
К чему это может привести? Получим знакомую ошибку:
TypeError: Cannot read properties of null (reading 'push')
В чем здесь проблема? Мы передали строку, которой не было в Map ('000'), поэтому получили в ответ null, а когда попытались вызвать push — получили ошибку.
Умный программист (каковым мы все хотели бы быть), вероятно, сделал бы что-то вроде этого:
const favouriteColors = userFavouriteColors('000') || [];
// Добавление нового цвета в список любимых цветов пользователя
favouriteColors.push('brown');
Теперь все замечательно: ошибки не будет. Но это означает, что без понимания внутреннего устройства вызываемой функции невозможно правильно обработать ее вывод.
Согласитесь, если бы существовал способ заранее узнать, что userFavouriteColors может вернуть null, вы бы тоже справились с этим случаем! Вот для этого и нужно обратиться к TypeScript.
Что такое TypeScript?
TypeScript — язык программирования, который является надмножеством JavaScript. Это означает, что любой правильный код на JavaScript является правильным кодом на TypeScript. Подробнее о том, как это работает «под капотом», поговорим позже, а пока рассмотрим простой код JavaScript:
let x = 10;
x = 'hello';
console.log(x);
Этот код будет работать нормально, а на выходе получим hello. С помощью TypeScript можно добавлять аннотации к переменным, обеспечивая их отношение к определенному типу. Допустим, в приведенном выше случае надо, чтобы x всегда был number (числом). Можно это записать так:
let x: number = 10;
x = 'hello';
console.log(x);
Если вы впервые знакомитесь с TypeScript, то : number будет для вас новшеством! Это аннотация типа, которая сообщает TypeScript, что x всегда должен быть number. Если присвоить x string (строку), получим ошибку:
Type 'string' is not assignable to type 'number'.
Теперь вы знаете, что x — всегда number, и если присвоить ему string, то TypeScript предупредит об ошибке.
Основные типы
В TypeScript есть несколько основных типов (basic types):
number: для любого числа, включая целые числа и числа с плавающей запятой);
string: для любого строкового значения;
boolean: для любых булевых значений (trueилиfalse);
symbol: для уникальных идентификаторов, создаваемых с помощьюSymbol()(часто используются в качестве ключей объектов — если вы раньше не использовали символы, не стоит слишком беспокоиться об этом);
null: для значенияnull;
undefined: для неопределенного (undefined) значения.
Если, помимо JavaScript, вы владеете другими языками, то, возможно, привыкли различать integers (целые числа) и floating point numbers (числа с плавающей точкой), а также strings (строки) и characters (символы).
Поскольку TypeScript является надмножеством JavaScript, а JavaScript не делает таких различий, TypeScript тоже их не делает. В JavaScript и 10, и 10.0 — это числа, и в нем нет понятия типа строки с одним символом, как в других языках.
Интерактивная среда TypeScript
Теперь попробуем запустить код на TypeScript! Настроить TypeScript в среде разработки довольно просто, но новичок может столкнуться с некоторыми трудностями. Масса статей посвящены тому, как настроить TypeScript локально, однако для изучения этой статьи можете открыть интерактивную среду TypeScript (TS playground) здесь. Это позволит вам написать код на TypeScript в браузере без необходимости настраивать среду разработки!
Попробуйте запустить следующий код в интерактивной среде TypeScript:
let n: number = 10;
let s: string = 'hello';
let b: boolean = true;
let nullValue: null = null;
let undefinedValue: undefined = undefined;
console.log(n.length);
console.log(s.length);
console.log(b.length);
Вы сразу увидите, что первая и третья строки подчеркнуты красным цветом. Это означает, что есть проблема. Довольно круто, правда? В ошибках будет написано что-то вроде: Property 'length' does not exist on type 'number' (Свойство «длина» не существует для типа «число»). Это ошибка, выходящая прямо из компилятора TypeScript, еще до запуска кода предупреждает, что он не сработает так, как ожидалось. Отлично!
Техническое замечание
Выше был упомянут «компилятор TypeScript». Дело в том, что TypeScript — компилируемый язык, то есть выполняемый код — фактически JavaScript, преобразованный из TypeScript-кода. В процессе компиляции TypeScript проверяет код на наличие ошибок и сообщает о них, как было показано в приведенном выше примере. Аннотации типов, добавляемые в TypeScript-код, не меняют его поведение во время выполнения — они лишь помогают TypeScript узнать, к какому типу относятся переменные.
Еще два типа: any и unknown
Есть еще два типа, о которых стоит знать: any и unknown.
any (любой) — тип, который отменяет все проверки типов для переменной. Таким образом, если присвоить переменной x тип any, TypeScript не будет проверять аннотации типов для x, и можно задать ей любое значение. Вы увидите, как это используется в кодовых базах, постепенно переходящих на TypeScript. Гораздо сложнее начинать с any и последовательно удалять аннотации типов, поэтому настоятельно рекомендую начинать с реальных типов, насколько это возможно.
unknown (неопределенный) — тип, который представляет любое значение. Его можно рассматривать как безопасную для типов версию any. Если попытаться сделать что-либо со значением unknown, придется выполнить несколько проверок во время выполнения, чтобы убедиться, что это безопасно. Подробнее об этом читайте в разделе «Сужение типов» этой статьи.
Массивы
Теперь, когда вам известны основные типы (number, string, boolean), рассмотрим, как использовать массивы (arrays) в TypeScript:
let numbers: number[] = [1, 2, 3];
let strings: string[] = ['a', 'b', 'c'];
let booleans: boolean[] = [true, false, true];
// Это приведет к ошибке:
booleans.push('hello');
Это должно выглядеть знакомо: обычно вы используете квадратные скобки для создания массивов в JavaScript, а в TypeScript будете создавать тип массива, используя имя типа, за которым следуют квадратные скобки ([]).
Кортежи
Кортеж (tuple) — особый тип массива, который стал возможен в TypeScript. Это массив с фиксированной длиной и фиксированным типом для каждого индекса. Вот пример:
type ColorCount = [number, string];
let redCount: ColorCount = [1, 'red'];
let invalidCount: ColorCount = ['hello', 'world']; // Эта строка приведет к ошибке
let invalidSizeCount: ColorCount = [1, 'red', 'blue']; // Эта строка тоже приведет к ошибке (слишком много элементов)
Этот кортеж имеет длину 2, первый элемент — number, второй — string. В строке invalidCount будет ошибка, потому что первый индекс был задан как string, а не number.
Функции
Функции в TypeScript состоят из нескольких подвижных частей. Рассмотрим полный пример и разберем каждую часть по отдельности.
const isGreaterThan = (a: number, b: number): boolean => {
return a > b;
}
// или эквивалентно:
function isGreaterThan(a: number, b: number): boolean {
return a > b;
}
Начнем с того, что поскольку функции JavaScript можно создавать двумя способами, то и синтаксис аннотирования функций в TypeScript немного отличается:
- Сначала в круглых скобках указываются параметры. Как видите, каждый параметр имеет свой тип.
- После скобок ставится символ двоеточия (
:), за которым следует возвращаемый тип функции. В данном случае можно сказать, чтоisGreaterThanвернет значениеboolean. Обратите внимание, что в синтаксисе стрелочных функций после возвращаемого типа ставится стрелка (=>).
Типизация функций как переменных
Выше было показано, как добавить аннотации типов к функции. Но что, если функция находится в переменной? Или является параметром другой функции? Рассмотрим следующий JavaScript-код:
const createUser = (id, onComplete) => {
const newUser = {
id,
};
onComplete(newUser);
}
Из этого примера следует, что createUser принимает два параметра: id и onComplete. id — string, а onComplete — функция, которая в качестве параметра принимает User и что-то с ним делает. Вот как можно написать этот код:
const createUser = (id: string, onComplete: (user: User) => void) => {
const newUser = {
id,
};
onComplete(newUser);
}
onComplete получает тип (user: User) => void. Это очень похоже на аннотацию функции как переменной: список параметров в круглых скобках, затем стрелка (=>) и возвращаемый тип. Вам еще не встречался void — тип, который означает, что функция ничего не возвращает. Можете написать код следующим образом:
type OnComplete = (user: User) => void;
const createUser = (id: string, onComplete: OnComplete) => {
const newUser = {
id,
};
onComplete(newUser);
}
Параметры по умолчанию
Чтобы правильно вводить функции с параметрами по умолчанию (default parameters), синтаксис должен выглядеть следующим образом:
const createUser = (id: string, name: string = 'John Doe') => {
return {
id,
name,
};
}
Остаточные параметры
Остаточные параметры (rest parameters) в JavaScript выглядят следующим образом:
const someFunction = (...args) => {
console.log(args);
}
// или с объектами:
const someFunction = ({ a, ...rest }) => {
console.log(a, rest);
}
Чтобы правильно ввести остаточные параметры, пишите TypeScript-код следующим образом:
const someFunction = (...args: number[]) => {
console.log(args);
}
// или с объектами:
const someFunction = ({ a, ...rest }: { a: number, b: number, c: number }) => {
console.log(a, rest); // `rest` is of type `{ b: number, c: number }`
}
Перегрузка функций
Перегрузка функций (function overloading) — расширенная возможность TypeScript, которая позволяет определять несколько функций с одинаковым именем и разными параметрами. Скорее всего, вам не понадобится слишком часто применять перегрузку функций. Просто посмотрите, как это примерно выглядит:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
Если вы только начинаете работать с TypeScript, не стоит слишком беспокоиться об этом!
Перечисляемый тип
Перечисляемый тип (enum) не имеет специальной нотации в TypeScript. Единственное, что можно сказать о перечисляемых типах, — это то, что при использовании TypeScript вам наверняка больше понравится применять типы объединения строк (сужу по собственному опыту). Ознакомьтесь с типами объединения строк в следующем разделе!
Комбинирование типов: типы объединения и пересечения
Вернемся к нашему исходному примеру функции userFavouriteColors:
const userColorMap = new Map({
'123': ['red', 'blue', 'green'],
'456': ['yellow', 'purple', 'orange'],
'789': ['pink', 'black', 'white'],
});
const userFavouriteColors = (userId) => {
return userColorMap.get(userId);
}
Мы знаем, что Map вернет либо массив строк, либо undefined. До сих пор вы имели дело только с отдельными типами, но TypeScript позволяет объединять типы с помощью символа вертикальной черты (|):
const userFavouriteColors = (userId: string): string[] | undefined => {
return userColorMap.get(userId);
}
В данном случае userFavouriteColors вернет либо string[] (массив строк), либо undefined. Это называется типом объединения (union type). Теперь попробуем сделать то, что требовалось изначально:
const userColorMap = new Map<string,string[]>({
'123': ['red', 'blue', 'green'],
'456': ['yellow', 'purple', 'orange'],
'789': ['pink', 'black', 'white'],
});
const userFavouriteColors = (userId: string): string[] | undefined => {
return userColorMap.get(userId);
}
const favouriteColors = userFavouriteColors('000');
favouriteColors.push('brown');
Испытайте этот код в интерактивной среде TypeScript и посмотрите, какую ошибку получите. У вас должно выйти примерно следующее:
'favouriteColors' is possibly 'undefined'.
Отлично, очень полезное предупреждение! Хотя подобную ошибку можно было бы предвидеть при написании JavaScript-кода, TypeScript помогает нам, сообщая, что favouriteColors, возможно, является undefined. Такое предупреждение избавит вас от многих проблем по мере роста сложного (и не только сложного) проекта.
Чтобы исправить описанную выше ошибку, можно написать что-то вроде этого:
const favouriteColors = userFavouriteColors('000');
if (favouriteColors) {
favouriteColors.push('brown');
}
Это лишь один из вариантов обработки ошибки. Можно также вызвать ошибку с помощью оператора throw, если favouriteColors является undefined или наблюдаются другие варианты поведения.
Типы пересечения (intersection types) работают аналогично, но функционируют как and («и»), а не как or («или») и используют символ &:
interface User {
id: string;
}
type UserWithRole = User & {
role: string;
}
const someUser: UserWithRole = {
id: '123',
role: 'admin',
}
Строковые типы объединения
Один из самых распространенных типов объединения — строковый тип объединения (string union type). Он нужен, когда требуется ограничить допустимые значения строки определенным набором значений. Допустим, вы пишете код для игры и хотите указать направление, в котором может двигаться игрок. Сделайте следующее:
type Direction = 'up' | 'down' | 'left' | 'right';
const move = (direction: Direction) => {
console.log(direction); // `direction` имеет тип `Direction`, поэтому мы уверены, что это будет одно из четырех значений
}
Сужение типа
Взгляните на приведенный ранее пример еще раз:
const favouriteColors = userFavouriteColors('000');
if (favouriteColors) {
favouriteColors.push('brown');
}
Если вы внимательный читатель, то должны задаться вопросом: как TypeScript узнает, что favouriteColors не является undefined внутри оператора if и позволяет push делать компиляцию без ошибок? Все дело в сужении типов (type narrowing)!
Когда TypeScript «читает» ваш код, он может выводить информацию о типах в зависимости от контекста. В этом примере, несмотря на то что тип favouriteColors — string[] | undefined внутри оператора if, TypeScript «знает»: поскольку мы проверили, что favouriteColors не является undefined, он должен быть string[].
Если навести курсор на favouriteColors внутри оператора if, вы увидите, что TypeScript сузил тип до string[], а если наведете курсор на favouriteColors вне оператора if, увидите, что TypeScript по-прежнему считает, что тип — string[] | undefined.
Читайте также:
- Почему крупные проекты отказываются от TypeScript?
- Совместное использование состояний между окнами без задействования сервера
- 3 альтернативы инструкции Switch в Typescript
Читайте нас в Telegram, VK и Дзен
Перевод статьи Joshua Saunders: TypeScript: Zero To Hero Plus Cheat Sheet





