Предисловие
Для вашего удобства текстовый курс перенесен на отдельную платформу. Ссылка для поступления: тык
Мы настоятельно рекомендуем пользоваться именно этой платформой по ходу курса. Ссылка на текущее занятие: тык
⚠️Прежде чем переходить к заданиям - обязательно зарегистрируйтесь на курс. Иначе вас закинет на общий поток и вы не сможете взаимодействовать с сверстниками во втором модуле курса. Это не смертельно и вполне решаемо, но тем не менее.
TypeScript, npm и Vite
Ошибки, которые JavaScript пропускает
JavaScript не проверяет, какие значения попадают в переменные и функции. Многие ошибки в коде он обнаруживает только во время выполнения, а некоторые не обнаруживает вовсе.
Посмотрите на функцию рендера карточки:
function renderCard(product) {
return `<h3>${product.tittle}</h3><p>${formatPrice(product.price)}</p>`;
}
В имени свойства опечатка: tittle вместо title. JavaScript не сообщит об ошибке. Свойства tittle у товара нет, выражение product.tittle даст undefined, и на карточке появится слово «undefined». Ошибку заметит пользователь, а не разработчик.
Вот ещё несколько ошибок, которые JavaScript пропустит до запуска:
formatPrice('89990'); // строка вместо числа
applyDiscount(products); // забыли второй аргумент — процент скидки
product.specs.ram; // у части товаров нет specs, и код упадёт с ошибкой
data.items.map(renderCard); // сервер присылает товары в data.products, а не в data.items
В небольшом скрипте такие ошибки находят быстро. В проекте из сотни файлов, над которым работают несколько человек, их находят, только когда что-то уже сломалось. Чтобы находить такие ошибки заранее, используют статическую типизацию: разработчик описывает, какие данные ожидаются в каждом месте программы, а специальная программа проверяет код до запуска.
Почитать ещё
Дока: Статическая типизация
Что такое TypeScript
TypeScript — это JavaScript с дополнительным синтаксисом для описания типов. Код на TypeScript хранят в файлах с расширением .ts.
function formatPrice(price: number): string {
return `${price.toLocaleString('ru-RU')} ₽`;
}
formatPrice('89990');
Запись price: number означает, что параметр price должен быть числом, а : string после скобок — что функция возвращает строку. Последняя строка содержит ошибку, и редактор подчеркнёт её красным ещё до запуска: Argument of type 'string' is not assignable to parameter of type 'number'.
Браузер не понимает TypeScript. Поэтому перед запуском код проходит компиляцию: компилятор tsc проверяет типы и превращает TypeScript в обычный JavaScript, удаляя из него всё, что относится к типам:
function formatPrice(price) {
return `${price.toLocaleString('ru-RU')} ₽`;
}
Типы существуют только во время разработки. В браузер попадает обычный JavaScript, поэтому типы не замедляют программу и не проверяют данные во время её работы.
Что даёт TypeScript:
Находит ошибки до запуска. Опечатки в именах, неправильные аргументы и забытые проверки на
nullвидны сразу в редакторе.Подсказывает. Если набрать
product., редактор покажет список свойств товара.Документирует код. Запись
toProduct(item: ApiProduct): Productсразу объясняет, что функция принимает и что возвращает.Упрощает изменения. Если переименовать свойство в типе, компилятор покажет все места, которые нужно исправить.
Весь второй модуль курса мы пишем на TypeScript. В большинстве команд фронтенд-разработки его используют по умолчанию.
Почитать ещё
TypeScript Playground — песочница, где можно писать TypeScript без установки
Типы переменных и вывод типов
Тип переменной записывают через двоеточие после её имени.
let count: number = 0;
let title: string = 'Laptop 14';
let inStock: boolean = true;
let tags: string[] = ['sale', 'new'];
let prices: number[] = [89990, 12490];
Основные типы совпадают с типами JavaScript: number, string, boolean, null, undefined. Массив записывают как тип элемента с квадратными скобками: string[] — массив строк.
Чаще всего тип указывать не нужно. TypeScript выводит его сам из начального значения:
let price = 89990;
price = 'дорого';
// Ошибка: Type 'string' is not assignable to type 'number'.
const products = ['Laptop 14', 'Мышь Click'];
products.push(42);
// Ошибка: Argument of type 'number' is not assignable to parameter of type 'string'.
Переменная price получила тип number из значения 89990, и присвоить ей строку уже нельзя.
Тип явно указывают там, где TypeScript не может его вывести или выводит не тот, который нужен:
у параметров функций, потому что значение появится только при вызове;
у переменных без начального значения;
у пустых массивов: для
const products = []TypeScript не знает, что будет лежать в массиве, поэтому пишутconst products: Product[] = [];у данных, которые приходят извне, например с сервера.
Почитать ещё
Everyday Types (англ.)
Типы объектов
Форму объекта описывают через ключевое слово type. Тип получает имя, и его можно использовать во всей программе.
type Product = {
id: number;
title: string;
price: number;
inStock: boolean;
image?: string;
};
const laptop: Product = {
id: 1,
title: 'Laptop 14',
price: 89990,
inStock: true,
};
laptop.tittle;
// Ошибка: Property 'tittle' does not exist on type 'Product'. Did you mean 'title'?
const mouse: Product = { id: 2, title: 'Мышь Click', price: 2490 };
// Ошибка: Property 'inStock' is missing in type '{ id: number; title: string; price: number; }' but required in type 'Product'.
Внутри фигурных скобок перечисляют свойства и их типы.
Знак
?после имени делает свойство необязательным. Тип свойстваimage—string | undefined: у товара может не быть картинки.Объект, который объявлен с типом
Product, должен содержать все обязательные свойства и не может содержать свойства, которых нет в типе.
Типы можно вкладывать друг в друга и использовать в массивах:
type Category = {
id: string;
title: string;
};
type CatalogPage = {
products: Product[];
category: Category;
total: number;
};
Имена типов пишут с заглавной буквы: Product, CatalogPage.
Кроме type, форму объекта можно описать через interface. Для объектов эти способы почти взаимозаменяемы. В курсе и в проекте ITAM Board мы используем type.
Почитать ещё
Object Types (англ.)
Объединения и литеральные типы
Объединение описывает значение, которое может иметь один из нескольких типов. Варианты перечисляют через вертикальную черту.
let error: string | null = null;
error = 'Сервер не отвечает';
error = 42;
// Ошибка: Type 'number' is not assignable to type 'string | null'.
Тип string | null говорит, что ошибки может не быть. Прежде чем работать с таким значением как со строкой, его нужно проверить. Как это делается, разберём в разделе про сужение типов.
Тип может состоять из конкретных значений. Такие типы называют литеральными:
type Status = 'idle' | 'loading' | 'ready' | 'error';
type Sort = 'price-asc' | 'price-desc';
let status: Status = 'idle';
status = 'loading';
status = 'sucess';
// Ошибка: Type '"sucess"' is not assignable to type 'Status'.
Переменной status можно присвоить только одну из четырёх строк. Опечатку 'sucess' компилятор найдёт сразу, а редактор подскажет допустимые варианты.
Объединения из строк встречаются в интерфейсах постоянно: статусы загрузки, варианты кнопок, способы сортировки, типы карточек. На седьмом занятии статус загрузки был обычной строкой, и опечатка в нём ломала рендер без всякой ошибки. С литеральным типом такая ошибка невозможна.
Почитать ещё
Union Types (англ.)
Типы функций
У функции типизируют параметры и возвращаемое значение.
function formatPrice(price: number): string {
return `${price.toLocaleString('ru-RU')} ₽`;
}
function applyDiscount(products: Product[], percent = 10): Product[] {
return products.map((product) => ({
...product,
price: Math.round(product.price * (1 - percent / 100)),
}));
}
function findProduct(products: Product[], id: number): Product | undefined {
return products.find((product) => product.id === id);
}
applyDiscount(products, '20');
// Ошибка: Argument of type 'string' is not assignable to parameter of type 'number'.
Типы параметров указывают всегда. Без них TypeScript не знает, что придёт в функцию, и в строгом режиме сообщает об ошибке
Parameter 'price' implicitly has an 'any' type.Если у параметра есть значение по умолчанию, тип выводится из него: у
percent = 10типnumber.Необязательный параметр отмечают знаком
?:function greet(name?: string).Возвращаемый тип TypeScript выводит из
return, но у функций, которые используют в других частях программы, его обычно пишут явно. Так тип служит документацией, а ошибку в теле функции компилятор покажет внутри функции, а не в местах вызова.
Функция, которая ничего не возвращает, имеет возвращаемый тип void.
Тип функции можно описать отдельно. Так типизируют колбэки:
type ProductFilter = (product: Product) => boolean;
const isAffordable: ProductFilter = (product) => product.price < 20000;
function filterProducts(products: Product[], filter: ProductFilter): Product[] {
return products.filter(filter);
}
В стрелочной функции isAffordable тип параметра product не указан: TypeScript взял его из типа ProductFilter.
Почитать ещё
More on Functions (англ.)
null и сужение типов
Многие ошибки в JavaScript начинаются с обращения к свойству значения, которое оказалось null или undefined: Cannot read properties of null. В строгом режиме TypeScript не даёт так сделать. В TypeScript 6 строгий режим включён по умолчанию.
const title = document.querySelector('h1');
title.textContent = 'Каталог';
// Ошибка: 'title' is possibly 'null'.
Метод querySelector возвращает тип HTMLHeadingElement | null, потому что элемента может не оказаться на странице. Прежде чем использовать значение, его нужно проверить:
const title = document.querySelector('h1');
if (title) {
title.textContent = 'Каталог';
}
title?.classList.add('title--active');
Внутри блока if (title) TypeScript знает, что title не null, и разрешает обращаться к его свойствам. Такое уточнение типа после проверки называется сужением. Сужают тип разные проверки:
function formatError(error: string | null): string {
if (error === null) {
return 'Ошибок нет';
}
return error.toUpperCase();
}
function formatValue(value: string | number): string {
if (typeof value === 'number') {
return value.toFixed(2);
}
return value.trim();
}
После return в первой ветке TypeScript понимает, что дальше error может быть только строкой.
Иногда разработчик знает, что значение точно есть, например элемент с id="app" всегда присутствует в index.html. Тогда после выражения ставят восклицательный знак: document.querySelector('#app')!. Он сообщает TypeScript, что значение не null, но никакой проверки в программу не добавляет. Если элемента всё-таки не окажется, код упадёт с ошибкой. Используйте ! только там, где отсутствие значения означает ошибку в самой разметке.
Почитать ещё
Narrowing (англ.)
any и unknown
Тип any отключает проверку. Значение с типом any можно использовать как угодно, и TypeScript не сообщит ни об одной ошибке:
let data: any = await response.json();
data.prodcts.map((item) => item.titel);
В этом коде две опечатки, но компилятор промолчит. Тип any превращает TypeScript обратно в JavaScript, поэтому в новом коде его не используют. Если тип данных известен, его описывают через type.
Если тип значения действительно неизвестен, используют unknown. Со значением типа unknown нельзя ничего сделать, пока тип не сужен проверкой:
try {
await loadProducts();
} catch (error) {
// error имеет тип unknown: выбросить можно что угодно, не только Error
const message = error instanceof Error ? error.message : 'Неизвестная ошибка';
console.error(message);
}
Переменная в блоке catch в строгом режиме имеет тип unknown, потому что в JavaScript можно выбросить не только объект ошибки, но и строку или число. Проверка error instanceof Error сужает тип, и после неё можно читать error.message.
Метод response.json() возвращает Promise<any>. Чтобы данные с сервера не превратили часть программы в нетипизированную, результат сразу присваивают переменной с описанным типом:
type ProductsResponse = {
products: ApiProduct[];
total: number;
};
const data: ProductsResponse = await response.json();
TypeScript не проверяет, что сервер действительно прислал данные такой формы. Тип — это обещание разработчика, основанное на документации API. На двенадцатом занятии мы будем генерировать такие типы из описания API автоматически.
Почитать ещё
The unknown Type (англ.)
Обобщённые типы
Некоторые типы принимают другой тип как параметр. Параметр записывают в угловых скобках.
const products: Array<Product> = [];
const titles: Array<string> = [];
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://dummyjson.com/products');
const data: ProductsResponse = await response.json();
return data.products.map(toProduct);
}
Array<Product>— то же, чтоProduct[], массив товаров.Promise<Product[]>— промис, который выполнится массивом товаров. Асинхронная функция всегда возвращает промис, поэтому её возвращаемый тип всегдаPromise<...>.
Такие типы называют обобщёнными (generics). Параметр можно передать и методам DOM. querySelector по селектору класса не знает, какой элемент найдёт, и возвращает общий тип Element. Уточнить тип можно параметром:
const form = document.querySelector<HTMLFormElement>('#add-form');
const input = document.querySelector<HTMLInputElement>('#title');
if (form && input) {
form.reset();
console.log(input.value);
}
Свою обобщённую функцию объявляют с параметром типа в угловых скобках после имени:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const firstProduct = first(products);
const firstTitle = first(['Laptop 14', 'Мышь Click']);
T — имя параметра типа. При вызове TypeScript подставляет вместо T тип элементов переданного массива: у firstProduct тип Product | undefined, у firstTitle — string | undefined. Одна функция работает с массивами любого типа и при этом сохраняет информацию о типе.
Почитать ещё
Generics (англ.)
Типы для элементов страницы
При переносе кода, который работает со страницей, на TypeScript чаще всего встречаются три ситуации.
Элементы, найденные по селектору. querySelector возвращает элемент или null. Если элемент обязательно есть в разметке, его находят один раз при запуске и сразу проверяют:
const list = document.querySelector<HTMLElement>('#catalog-list');
const form = document.querySelector<HTMLFormElement>('#add-form');
if (!list || !form) {
throw new Error('В разметке нет каталога или формы');
}
После такой проверки TypeScript знает, что list и form не null, во всём коде ниже.
Цель события. Свойство event.target имеет тип EventTarget | null, потому что событие может произойти не только на элементе. Прежде чем вызывать closest, тип сужают проверкой instanceof:
list.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) {
return;
}
const button = event.target.closest<HTMLButtonElement>('.card__like');
const card = button?.closest<HTMLElement>('.card');
if (card) {
toggleLike(Number(card.dataset.productId));
}
});
Поля формы. Удобно читать значения полей через FormData. Метод get возвращает тип FormDataEntryValue | null: значение может оказаться файлом или отсутствовать. Поэтому значение превращают в строку явно:
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const title = String(formData.get('title') ?? '').trim();
const price = Number(formData.get('price'));
addProduct(title, price);
});
FormData собирает значения всех полей формы с атрибутом name.
Типы элементов называются по тегам: HTMLInputElement, HTMLButtonElement, HTMLFormElement, HTMLImageElement. У каждого типа свои свойства: value есть у HTMLInputElement, но нет у HTMLElement.
Почитать ещё
DOM Manipulation (англ.)
Node.js
Код, который мы писали до сих пор, выполнял браузер. Но компилятор TypeScript, сборщик и другие инструменты разработки — это тоже программы на JavaScript, и им нужна среда выполнения вне браузера. Такой средой служит Node.js.
Установите актуальную LTS-версию с сайта nodejs.org. LTS означает версию с долгой поддержкой: она стабильна, и её используют в большинстве проектов.
Проверьте установку в терминале. В VS Code терминал открывается через пункт меню Terminal → New Terminal:
node --version
npm --version
Обе команды должны вывести номера версий. Если терминал пишет, что команда не найдена, закройте и заново откройте VS Code: терминал должен узнать о новой программе.
Node.js умеет выполнять файлы с JavaScript. Создайте файл hello.js с командой console.log('Привет из Node.js') и выполните в терминале:
node hello.js
Сообщение появится прямо в терминале. В Node.js нет document и window, потому что нет страницы. Зато есть доступ к файлам компьютера и сети, поэтому на Node.js пишут серверы и инструменты разработки.
Вместе с Node.js устанавливается npm — менеджер пакетов, о котором следующий раздел.
Почитать ещё
Дока: Node.js
npm и пакеты
Пакет — готовая библиотека или инструмент, который можно подключить к проекту. Пакеты публикуют в общий реестр npm, где их сотни тысяч: React, TypeScript, Vite, библиотеки для дат, форм и графиков.
Менеджер пакетов npm скачивает пакеты из реестра и следит за их версиями.
npm install zustand
npm install -D typescript
npm install
npm uninstall zustand
npx create-vite@latest
npm install имяскачивает пакет в папкуnode_modulesпроекта и записывает его в список зависимостей вpackage.json.Флаг
-Dзаписывает пакет в зависимости для разработки. Разница между видами зависимостей — в следующем разделе.npm installбез имени устанавливает все зависимости изpackage.json. Эту команду выполняют после того, как скачали чужой проект или создали новый.npm uninstall имяудаляет пакет.npx имязапускает команду из пакета, не добавляя пакет в проект.
У пакетов есть свои зависимости, и npm устанавливает их тоже. Поэтому папка node_modules даже в небольшом проекте весит сотни мегабайт. Её не отправляют другим разработчикам и не добавляют в репозиторий: любой может восстановить её командой npm install.
Кроме npm, есть другие менеджеры пакетов: pnpm и yarn. Они решают те же задачи с немного другими командами. В курсе мы используем npm.
Почитать ещё
Дока: Пакетные менеджеры
Документация npm (англ.)
package.json
Файл package.json в корне проекта описывает проект: его имя, команды и зависимости.
{
"name": "catalog",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"zustand": "^5.0.15"
},
"devDependencies": {
"typescript": "~6.0.2",
"vite": "^8.3.0"
}
}
scripts— команды проекта. Командаnpm run buildвыполнит то, что записано вbuild. Благодаря этому в любом проекте не нужно помнить длинные команды: разработку запускаетnpm run dev, сборку —npm run build.dependencies— пакеты, которые нужны приложению во время работы у пользователя. Их код попадает в собранный сайт.devDependencies— пакеты, которые нужны только при разработке: компилятор, сборщик, линтер. В собранный сайт они не попадают."type": "module"сообщает Node.js, что файлы проекта используют синтаксис модулейimportиexport.
Версия записывается тремя числами: 8.3.0 — это основная версия, дополнительная и исправление. Символ перед версией говорит, какие обновления разрешены:
^8.3.0разрешает любые версии8.x.xне ниже8.3.0, но не9.0.0;~6.0.2разрешает только исправления:6.0.3, но не6.1.0.
Рядом с package.json npm создаёт файл package-lock.json. В нём записаны точные версии всех установленных пакетов, включая зависимости зависимостей. Этот файл сохраняют вместе с проектом, чтобы у всех разработчиков установились одинаковые версии.
Почитать ещё
Документация npm: package.json (англ.)
Модули: import и export
В проекте на TypeScript код делят на файлы-модули. Каждый модуль явно сообщает, что он предоставляет другим модулям, и явно подключает то, что ему нужно.
// src/format.ts
export const CURRENCY = '₽';
export function formatPrice(price: number): string {
return `${price.toLocaleString('ru-RU')} ${CURRENCY}`;
}
function round(value: number): number {
return Math.round(value);
}
// src/types.ts
export type Product = {
id: number;
title: string;
price: number;
};
// src/main.ts
import { formatPrice } from './format';
import type { Product } from './types';
const laptop: Product = { id: 1, title: 'Laptop 14', price: 89990 };
console.log(formatPrice(laptop.price));
exportперед объявлением делает функцию, переменную или тип доступными другим модулям.import { имя } from './путь'подключает экспортированные значения. Путь указывают относительно текущего файла, расширение.tsможно не писать.import typeподключает только типы. В шаблоне Vite включена настройка, которая требует импортировать типы именно так: такие импорты полностью удаляются при компиляции.Функция
roundне экспортирована, поэтому снаружи модуля её не видно.
Переменные модуля видны только внутри него. В отличие от обычного script.js, где все переменные попадали в общую область видимости, модули не мешают друг другу, даже если в них есть переменные с одинаковыми именами.
Кроме именованных экспортов, у модуля может быть один экспорт по умолчанию export default, который импортируют без фигурных скобок. В курсе мы используем только именованные экспорты: так имя функции одинаковое во всех файлах, где её используют.
Почитать ещё
Сборщики и Vite
Проект на TypeScript нельзя просто открыть в браузере через Live Server:
браузер не понимает TypeScript, код нужно скомпилировать;
модулям нужны пакеты из
node_modules, а браузер не знает, где их искать;стили, картинки и другие файлы тоже подключают через
import, и браузер не умеет импортировать их из JavaScript;сотни небольших модулей загружаются медленнее, чем несколько готовых файлов, а код для публикации нужно сжать.
Эти задачи решает сборщик. Он находит все модули, начиная с точки входа, компилирует их, подключает пакеты и файлы и собирает из них готовый сайт из нескольких HTML, CSS и JavaScript файлов.
В курсе мы используем сборщик Vite. Он работает в двух режимах:
Режим разработки запускает локальный сервер, похожий на Live Server. Когда вы сохраняете файл, Vite обновляет в браузере только изменённый модуль, без перезагрузки страницы. Эта возможность называется HMR (Hot Module Replacement).
Сборка создаёт папку
distс готовым сайтом: код скомпилирован, объединён в небольшое число файлов и сжат. Содержимое этой папки публикуют на сервере.
Раньше самым популярным сборщиком был webpack, и он до сих пор встречается в существующих проектах. Новые проекты чаще создают на Vite: он быстрее запускается и почти не требует настройки.
Почитать ещё
Дока: Сборщики
Vite: почему Vite (англ.)
Проект на Vite
Новый проект создают одной командой. Флаг --template выбирает шаблон: vanilla-ts — TypeScript без фреймворков, react-ts — React и TypeScript.
npm create vite@latest catalog -- --template vanilla-ts
cd catalog
npm install
npm run dev
Первая команда создаст папку catalog с заготовкой проекта, вторая перейдёт в неё, третья установит зависимости, а четвёртая запустит сервер разработки. В терминале появится адрес, обычно http://localhost:5173.
Структура проекта:
catalog/
├── index.html главная страница, в ней подключён src/main.ts
├── package.json
├── tsconfig.json настройки TypeScript
├── public/ файлы, которые копируются в сборку без изменений
└── src/
├── main.ts точка входа: с этого модуля Vite начинает сборку
├── counter.ts пример модуля из шаблона
├── style.css
└── assets/ картинки, которые подключают через import
В index.html точка входа подключена так: <script type="module" src="/src/main.ts"></script>. Всё остальное подключается из main.ts через import, в том числе стили: import './style.css'.
Команды проекта:
npm run dev— сервер разработки с HMR;npm run build— проверка типов командойtscи сборка в папкуdist. Если в коде есть ошибки типов, сборка остановится;npm run preview— локальный сервер для собранной версии изdist, чтобы проверить её перед публикацией.
Ошибки типов показывает редактор, но npm run dev их не проверяет: сервер разработки только удаляет типы и запускает код. Поэтому перед отправкой работы и перед публикацией обязательно запускайте npm run build.
Папки node_modules и dist уже перечислены в файле .gitignore шаблона и в репозиторий не попадают.
Почитать ещё
Vite: начало работы (англ.)
Переменные окружения
Некоторые значения зависят от того, где запущен проект. Например, во время разработки приложение может работать с тестовым сервером, а после публикации — с настоящим. Такие значения не записывают в код, а передают через переменные окружения.
Vite читает переменные окружения из файлов .env в корне проекта:
# .env.local
VITE_API_URL=https://dummyjson.com
// src/api.ts
const API_URL = import.meta.env.VITE_API_URL ?? 'https://dummyjson.com';
Переменные записывают в файле построчно в виде
ИМЯ=значение.В коде приложения доступны только переменные, имя которых начинается с
VITE_. Остальные переменные Vite не передаёт в браузер, чтобы случайно не опубликовать секретные данные.Значения читают через объект
import.meta.env.Файл
.env.localпредназначен для настроек конкретного компьютера и не попадает в репозиторий. Файл.envбез суффикса попадает в репозиторий и хранит значения по умолчанию.
После изменения файла .env сервер разработки нужно перезапустить.
Всё, что попало в код приложения, попадает в браузер пользователя. Переменные окружения с приставкой VITE_ не подходят для паролей и секретных ключей: любой пользователь найдёт их в собранных файлах.