Студенческий материал. Его можно использовать как самостоятельный конспект, раздаточный материал и справочник после занятия.
Контекст. Перед тем как собирать интерфейс (кнопки, списки, формы), нам нужен «язык общения» с браузером. Этим языком и служат JavaScript и его надстройка TypeScript.
Когда пользователь нажимает кнопку, мы должны выполнить функцию. Когда приходит ответ API, нужно разобрать данные. Когда задача меняет статус, необходимо обновить состояние интерфейса. Всё это выражается средствами JavaScript (мы увидим это во второй половине урока).
Angular и React не скрывают JavaScript. Напротив, они предполагают, что разработчик уверенно владеет функциями, объектами, массивами, модулями и асинхронным кодом.
Программа состоит из команд, которые можно рассматривать как инструкции. Выражение, в свою очередь, вычисляется в значение.
Контекст. Это самый первый базовый навык: отличить «кусок кода, который что-то считает» (выражение) от «команды сделать что-то» (инструкция). В TaskFlow всё строится на этом.
10 + 20: его можно «свернуть» в ответ 30. Инструкция — как команда «запиши результат в тетрадь под именем total». Одно что-то считает, другое что-то делает.const total = 10 + 20;
const greeting = "Привет";
console.log(total);
console.log(greeting);
Разбор по шагам:
10 + 20 — выражение, оно вычисляется в число 30.const total = ... — инструкция: создаём «ящичек» total и кладём туда 30.console.log(total) — инструкция: показываем содержимое ящичка в консоли (30).10 + 20 — выражение: оно даёт значение 30. const total = 10 + 20; — инструкция объявления и инициализации переменной.
Что увидит пользователь: в консоли браузера (F12 → Console) появятся две строки: 30 и Привет.
Это различие пригодится позже, когда JSX, Angular template expressions и callback-функции начнут смешивать вычисления и управление потоком.
Переменная — имя, под которым программа хранит ссылку на значение. В современном JavaScript чаще используются const и let. const означает, что переменную нельзя переназначить. Объект, на который она указывает, при этом может быть изменён.
Контекст. Переменная — это фундамент всего кода. В TaskFlow мы храним в переменных список задач, статус загрузки, выбранный фильтр.
const — это ячейка, которую нельзя переназначить на другой шкафчик (но вещи внутри можно поменять). let — ячейка, которую можно «переклеить» на другой шкаф.const task = { title: "Изучить TS" };
task.title = "Изучить TypeScript"; // допустимо: меняем вещь внутри ячейки
// task = {}; // ошибка: переменную нельзя переназначить на другой объект
let count = 0;
count = count + 1; // допустимо: let разрешает переназначить
Разбор по шагам:
task — объект с заголовком.task новый объект — ошибка, потому что const.count (тип let) такое разрешено.Область видимости означает, где имя доступно. let и const имеют блочную область видимости, связанную с фигурными скобками.
{ }), то снаружи этой бумажки не видно.if (true) {
const message = "Внутри блока";
console.log(message);
}
// console.log(message); // ошибка: снаружи блока имя не видно
Что увидит пользователь: на экране консоли появится Внутри блока. Если раскомментировать последнюю строку — ошибка «message is not defined».
const. Используйте let, когда значение действительно должно быть переназначено.Основные примитивы, с которыми вы будете постоянно работать во frontend: string, number, boolean, null, undefined, symbol и bigint. На практике для TaskFlow чаще всего нужны строки, числа и логические значения. Symbol и bigint встретятся реже и будут показаны при необходимости.
Контекст. Тип данных — это «вид вещи», которую мы храним. От типа зависит, что с вещью можно делать.
const title = "Изучить Angular"; // string — текст в кавычках
const count = 3; // number — число
const completed = false; // boolean — true или false
const emptyValue = null; // null — «значения нет, и это подтверждено»
let notAssigned; // undefined — «значение ещё не присвоено»
Что увидит пользователь: сами по себе эти строки ничего не выводят, но каждая переменная теперь «знает своё место» в программе.
JavaScript динамически типизирован во время выполнения: одна переменная может получить значение другого типа. TypeScript помогает обнаруживать часть таких проблем ещё до запуска.
Оператор === проверяет строгое равенство без неявного приведения типов между разными типами. В современном коде frontend именно он обычно предпочтителен.
Контекст. В TaskFlow мы сравниваем, например, task.completed === true, чтобы понять, выполнена ли задача.
=== — строгий контролёр на входе: «ваш билет должен быть именно таким». А == — добродушный, но рассеянный: может пропустить человека с билетом другого цвета, решив, что «в принципе подходит». Отсюда неожиданности.5 === 5 // true — одинаковые числа
5 === "5" // false — число против текста: типы разные
5 == "5" // true — нестрогое сравнение «простило» разницу типов
5 === "5" // false
Что увидит пользователь: в консоли true, false, true, false — именно в таком порядке.
На первый взгляд == кажется удобным, но неявные преобразования становятся источником неожиданных результатов. Поэтому начинающим разработчикам проще придерживаться строгих сравнений.
null == undefined возвращает true, но null === undefined — false. Многие стилистические правила (например, рекомендуемые правила ESLint) запрещают использование == полностью.if позволяет выполнить код только при выполнении условия. Guard clause — ранняя проверка, которая сразу завершает функцию при невозможном или нежелательном состоянии.
Контекст. В TaskFlow функция «отметить задачу выполненной» сначала проверяет: а существует ли задача вообще?
function completeTask(task) {
if (!task) {
return; // охранник: задачи нет — выходим сразу
}
if (task.completed) {
return; // уже выполнена — выходим, менять нечего
}
task.completed = true; // только здесь — основная работа
}
Разбор по шагам:
completed = true.Guard clauses уменьшают вложенность. В больших компонентах это особенно удобно: сначала обрабатываются ошибочные случаи, затем основной сценарий.
Классический цикл for полезен, когда нужен контроль над индексом или количеством шагов. Но в frontend часто важнее выразить операцию над коллекцией через методы массива.
Контекст. В TaskFlow нужно перебрать все задачи, чтобы, например, показать их списком на экране.
for — как пересчитать ступеньки по номерам: «ступенька 0, ступенька 1, ступенька 2…». А for...of — как просто обойти все вещи на полке по очереди, не думая о номерах.// предположим, что tasks — массив задач из раздела 8
for (let i = 0; i < tasks.length; i++) {
console.log(tasks[i].title); // выводим заголовок по индексу i
}
for (const task of tasks) {
console.log(task.title); // перебираем сами объекты задач
}
Что увидит пользователь: в консоли напечатаются заголовки всех задач (оба цикла дадут один и тот же результат, просто разными способами).
На следующем этапе мы увидим map, filter и find. Они лучше описывают намерение программы.
Массив хранит упорядоченную коллекцию значений. В TaskFlow это список задач.
Контекст. Массив — главная «таблица данных» нашего приложения: в нём лежат все задачи по порядку.
tasks[0] — самый первый пассажир.const tasks = [
{ id: 1, title: "Изучить HTTP", completed: true },
{ id: 2, title: "Изучить Angular", completed: false },
{ id: 3, title: "Изучить React", completed: false }
];
console.log(tasks.length); // сколько всего задач (3)
console.log(tasks[0]); // первая задача (с id: 1)
tasks.push({ id: 4, title: "Git", completed: false }); // добавили задачу в конец
Разбор по шагам:
tasks.length — число элементов (3).tasks[0] — доступ к первому элементу по индексу.push — «прицепляет» новую задачу в хвост массива.Что увидит пользователь: в консоли 3, затем объект первой задачи, а массив теперь содержит 4 задачи.
Индекс начинается с нуля. Поэтому tasks[0] — первая задача.
Эти методы становятся частью ежедневного языка frontend-разработчика.
Контекст. В TaskFlow: filter оставит только активные задачи, map превратит их в заголовки, find найдёт одну нужную, reduce посчитает, сколько выполнено.
map — как конвейер (или ксерокс): каждый элемент проходит через одну и ту же операцию и выходит преобразованным (один вход → один выход).filter — как ситечко для чая: пропускает только нужное, остальное отбрасывает.find — как поиск по номеру: вы называете номер (например, id), и находится ровно один элемент с этим номером.reduce — как накопительный итог: значения по очереди складываются в один общий результат.const titles = tasks.map(task => task.title);
// ["Изучить HTTP", "Изучить Angular", "Изучить React"]
const active = tasks.filter(task => !task.completed);
// [объекты с completed: false]
const selected = tasks.find(task => task.id === 2);
// { id: 2, title: "Изучить Angular", completed: false }
const completedCount = tasks.reduce(
(count, task) => count + (task.completed ? 1 : 0),
0
); // 1
Разбор по шагам (на примере reduce):
count = 0 (второй аргумент reduce).count = количество выполненных.Что увидит пользователь: titles — массив строк, active — массив невыполненных задач, selected — одна задача, completedCount = 1.
map создаёт новую коллекцию преобразованных значений. filter оставляет подходящие элементы. find ищет первый подходящий элемент. reduce сворачивает коллекцию в одно итоговое значение.
find() возвращает undefined, если элемент не найден. Всегда проверяйте результат перед использованием.map(). Почему это важно для Angular? Те же данные будут проходить через template control flow и computed значения.Объект объединяет связанные значения по именованным свойствам.
Контекст. В TaskFlow каждая задача — это объект: у неё есть id, заголовок и статус.
const task = {
id: 1,
title: "Изучить TypeScript",
completed: false
};
console.log(task.title); // читаем свойство
task.completed = true; // меняем свойство
Что увидит пользователь: console.log напечатает Изучить TypeScript, а после присваивания задача считается выполненной.
Объекты — основа данных frontend. API возвращает объекты, формы создают объекты, компоненты получают объекты, state хранит объекты.
Деструктуризация позволяет получить значения из объекта или массива по имени или позиции.
Контекст. Вместо того чтобы писать task.title снова и снова, мы «вытаскиваем» нужные поля сразу.
const task = {
id: 1,
title: "Изучить TypeScript",
completed: false
};
const { id, title, completed } = task; // достали три поля сразу
console.log(title); // можно использовать напрямую
Разбор по шагам:
task.title, id, completed — отдельные переменные.В React вы постоянно встретите конструкцию вроде function TaskItem({ task }). Это деструктуризация параметра функции. В Angular деструктуризация используется аналогично, например при обработке данных внутри метода класса компонента: const { title, completed } = this.task;.
Оператор spread ... позволяет развернуть элементы. При работе с объектами он часто используется для создания обновлённой копии.
Контекст. В TaskFlow, когда мы меняем задачу, важно не «портить» старую, а сделать новую копию с изменением.
... — как ксерокс: вы копируете все страницы старой папки и на одной из них делаете правку. Старая папка остаётся нетронутой.// task = { id: 1, title: "Изучить TypeScript", completed: false }
const updatedTask = {
...task, // скопировали все поля старой задачи
completed: true // и перезаписали одно поле
};
Что увидит пользователь: updatedTask равен { id: 1, title: "Изучить TypeScript", completed: true }, а исходный task не изменился.
Это особенно важно для state: вместо изменения исходного объекта мы создаём новый объект с нужным изменением.
labels.function logTask(id, ...labels) {
console.log(id);
console.log(labels);
}
logTask(1, "важно", "срочно"); // labels = ["важно", "срочно"]
В rest-параметр попадают остальные аргументы.
Функция — основной способ организации поведения программы. Она получает входные значения, выполняет действия и может вернуть результат.
Контекст. В TaskFlow функция toggleTask переключает статус задачи «выполнена/не выполнена».
function toggleTask(task) {
return {
...task,
completed: !task.completed // инвертируем: было true → стало false, и наоборот
};
}
const next = toggleTask({
id: 1,
title: "Learn",
completed: false
});
Разбор по шагам:
toggleTask с задачей.completed.Что увидит пользователь: next будет { id: 1, title: "Learn", completed: true }.
Хорошая функция делает одну понятную операцию и имеет ясный контракт.
Arrow function — компактный синтаксис функции.
Контекст. В TaskFlow мы пишем их внутри map/filter: tasks.filter(task => !task.completed).
const add = (a, b) => a + b; // одно выражение: return не нужен
const titles = tasks.map(task => task.title); // достаём заголовки
const active = tasks.filter(task => !task.completed); // только активные
// стрелочная функция с несколькими инструкциями
const processTask = (task) => {
const updated = { ...task };
updated.completed = true;
return updated; // здесь return обязателен
};
Разбор по шагам:
=>, результат возвращается автоматически.{} — нужно писать return явно.Arrow functions особенно часто используются в callback-операциях. Обратите внимание на отсутствие return в стрелочной функции с одним выражением и наличие фигурных скобок и явного return в функции с несколькими инструкциями.
(click)="handleClick()"), потому что Angular не отслеживает изменения при передаче стрелочной функции.Callback — функция, которую передали другой функции, чтобы она была вызвана позже. Массивы, события и асинхронные операции используют этот паттерн повсеместно.
Контекст. В TaskFlow: когда пользователь нажимает «выполнить», мы передаём функцию-обработчик, которая сработает в момент клика.
function runOperation(operation) {
operation(); // вызываем переданную функцию в нужный момент
}
runOperation(() => {
console.log("Выполняем операцию");
});
Что увидит пользователь: в консоли появится Выполняем операцию — потому что runOperation вызвала переданный callback.
React event handlers и многие методы массивов используют callback-функции. Angular template также приводит нас к обработчикам событий, которые вызывают методы класса.
Замыкание возникает, когда функция сохраняет доступ к переменным внешней области видимости даже после завершения внешней функции.
Контекст. В TaskFlow это пригодится, чтобы «запомнить» состояние между действиями, например счётчик выполненных задач.
function createCounter() {
let count = 0; // «блокнот» в кармане
return function () {
count++; // каждый вызов прибавляет 1
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Разбор по шагам:
createCounter создаёт переменную count = 0 и возвращает внутреннюю функцию.count (замыкание).counter() увеличивает и возвращает счётчик.Что увидит пользователь: 1, затем 2.
Это полезно для понимания private-состояния, callback-функций и некоторых паттернов Hooks. Например, в React хук useState использует замыкание для хранения значения между рендерами компонента.
this в JavaScript зависит от контекста вызова. Для новичка важно не пытаться вывести одно универсальное правило из всех случаев. Лучше понимать разницу между обычной функцией и arrow function.
Контекст. В объекте user метод sayHello обращается к this.name — то есть к имени самого пользователя.
this — как слово «я» в разговоре. Если вы говорите «я устал» в своей комнате — понятно, о ком речь. Но если записать ваши слова на автоответчик и проиграть в чужой компании, «я» может указывать не туда. Так this зависит от того, «где» его произнесли.const user = {
name: "Anna",
sayHello() {
console.log(this.name); // this → user, поэтому "Anna"
}
};
user.sayHello(); // Anna
// Разница с arrow function:
const user2 = {
name: "Bob",
delayedHello() {
setTimeout(function () { console.log(this.name); }, 100);
// undefined — обычная функция потеряла контекст user2
},
delayedHelloArrow() {
setTimeout(() => { console.log(this.name); }, 100);
// "Bob" — arrow захватывает внешний this
}
};
Что увидит пользователь: user.sayHello() напечатает Anna; в delayedHello через таймер придёт undefined, а в delayedHelloArrow — Bob.
В современных React function components мы часто избегаем ручной работы с this. В Angular класс компонента использует обычный контекст экземпляра, поэтому там проблема выглядит иначе.
Модуль позволяет разделить программу на файлы. Каждый файл может экспортировать значения и импортировать зависимости.
Контекст. В TaskFlow тип Task мы вынесем в отдельный файл и будем «подключать» его, где нужно.
// task.ts
export type Task = {
id: number;
title: string;
completed: boolean;
};
// task-utils.ts
import type { Task } from "./task"; // берём тип Task из соседнего файла
export function toggleTask(task: Task): Task {
return { ...task, completed: !task.completed };
}
И Angular, и React активно используют ES modules. Поэтому import/export нужно знать уверенно.
type в import type и export type — особенность TypeScript, о которой мы поговорим подробнее позже.import связывает текущий модуль с экспортом другого модуля. В TypeScript часто используется import type для импорта только типа.
Контекст. В TaskFlow мы пишем import type { Task } from "./types", чтобы просто «знать форму» задачи, не подключая лишний код.
import — как взять чужой чертёж в руки, чтобы по нему собрать деталь. import type — как посмотреть только размеры на чертеже, саму деталь не забирая.import { useState } from "react"; // значение (функция) — нужно браузеру
import type { Task } from "./types"; // только описание типа — для проверки
Разделение типов и runtime-значений помогает понимать, что действительно понадобится браузеру, а что существует только на этапе проверки типов.
Promise представляет результат асинхронной операции, который будет известен позже. Promise может завершиться успешно или с ошибкой.
Контекст. В TaskFlow, когда мы загружаем задачи с сервера, ответ приходит не мгновенно — Promise «обещает» его дать позже.
const result = fetch("https://example.com"); // result — это Promise (талончик)
result
.then(response => response.text()) // когда готово — берём текст
.then(html => console.log(html)) // и печатаем
.catch(error => console.error(error)); // если ошибка — ловим её
Разбор по шагам:
fetch сразу возвращает Promise, не дожидаясь ответа..then срабатывает, когда данные пришли успешно..catch срабатывает, если что-то пошло не так.MDN описывает Promise как основу современной асинхронной модели JavaScript; async/await строится поверх Promise.
Синтаксис async/await позволяет писать асинхронный код в форме, которая напоминает последовательную программу.
Контекст. В TaskFlow загрузка задач с сервера выглядит как обычная последовательность: «подожди ответ, прочитай JSON, верни результат».
await — как ожидание посылки по обещанию: вы «ждёте, пока придёт результат», и только когда он готов, идёте дальше. Но при этом вся остальная программа (браузер) продолжает работать.async function loadTasks() {
const response = await fetch("http://localhost:3000/tasks"); // ждём ответ
const tasks = await response.json(); // ждём превращения в данные
return tasks; // возвращаем готовый массив
}
Что увидит пользователь: функция вернёт Promise, который в итоге даст массив задач (если сервер ответил).
await приостанавливает выполнение текущей async-функции до завершения Promise. Он не блокирует весь браузер.
Для сетевого и пользовательского кода ошибка — нормальный сценарий, а не исключение, которое можно игнорировать.
Контекст. В TaskFlow загрузка может упасть (сервер лёг, нет сети) — мы должны это перехватить и показать сообщение.
try/catch — как попытка перейти улицу: «попробуй перейти (try), а если машина — отскочи (catch)». Программа не падает насовсем, а просто реагирует на проблему.async function loadTasks() {
try {
const response = await fetch("http://localhost:3000/tasks");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`); // сами бросаем ошибку
}
return await response.json();
} catch (error) {
console.error("Не удалось загрузить задачи", error);
throw error; // пробрасываем дальше, чтобы вызвавший код тоже узнал
}
}
Разбор по шагам:
try пытаемся загрузить данные.throw.catch ловит любую ошибку и сообщает о ней.Обратите внимание: fetch() не выбрасывает исключение только потому, что сервер вернул HTTP 404 или 500. Поэтому проверяем response.ok самостоятельно.
fetch связывает JavaScript с HTTP. Frontend формирует запрос, получает Response и преобразует тело ответа, например, через response.json().
Контекст. В TaskFlow fetch — это «телефон» к серверу: мы звоним по адресу и просим список задач.
fetch — как заказ в магазине: вы послали заявку, получили посылку (response) или отказ. Саму посылку (тело ответа) ещё нужно распаковать — методом .json().// эти строки должны быть внутри async-функции
const response = await fetch("http://localhost:3000/tasks"); // звоним на сервер
const tasks = await response.json(); // распаковываем посылку в данные
console.log(tasks);
Что увидит пользователь: в консоли — массив задач, присланных сервером (или ошибка, если сервер не отвечает).
Браузер предоставляет JavaScript API: DOM, events, fetch, timers, storage и другие возможности. Язык JavaScript сам по себе не определяет DOM. DOM и fetch предоставляет среда браузера.
Контекст. Понимание границы «что есть в языке, а что дал браузер» помогает не путаться, почему один и тот же код работает в браузере, но не в Node.js.
| Возможность | Откуда берётся | Пример |
|---|---|---|
| Array, Object, Promise | JavaScript language/runtime | tasks.map(...) |
| document | Browser API | document.querySelector |
| fetch | Web API | fetch(url) |
| setTimeout | Web API | setTimeout(fn, 1000) |
Эта граница полезна, когда одна и та же программа запускается в браузере и в Node.js: набор доступных глобальных API отличается.
TypeScript проверяет JavaScript-программы до запуска и позволяет описать ожидаемые типы.
Официальный TypeScript Handbook определяет TypeScript как статический type checker для JavaScript-программ: он работает до выполнения и проверяет корректность типов.
Контекст. В TaskFlow TypeScript следит, чтобы мы случайно не передали в задачу число вместо текста.
function greet(name: string) {
return `Привет, ${name}`;
}
greet("Анна");
// greet(123); // ошибка типов: ожидается строка, а не число
Что увидит пользователь: рабочий вызов напечатает Привет, Анна; закомментированный вызов выдаст ошибку ещё до запуска в редакторе.
TypeScript не превращает JavaScript в «безопасный язык» и не проверяет реальный JSON с сервера автоматически. Он делает статические предположения, которые нужно поддерживать корректными.
Для ежедневного frontend достаточно сначала уверенно знать string, number, boolean, массивы, объекты, union types и функции. Официальная документация TypeScript отдельно разбирает примитивы и функции как базовые конструкции.
Контекст. В TaskFlow: заголовок — string, количество — number, статус — boolean.
let title: string = "TaskFlow"; // только текст
let count: number = 10; // только число
let completed: boolean = false; // только да/нет
let tags: string[] = ["web", "frontend"]; // массив только из текста
В JavaScript число не разделяется на типы integer и float на уровне языка; в TypeScript соответствующий тип — number.
Annotation — явное указание типа. Inference — способность TypeScript вывести тип самостоятельно.
Контекст. В TaskFlow часто пишут просто const count = 10 — и TypeScript сам понимает, что это number.
const title: string = "Angular"; // annotation — подписали явно
const count = 10; // inference — TypeScript выводит number сам
const task = {
id: 1,
title: "React",
completed: false
}; // TypeScript вывел форму объекта автоматически
Не нужно указывать тип там, где TypeScript очевидно его выводит. Ценность типов растёт на границах: функции, публичные API, модели данных и сложные структуры.
Типизация коллекций помогает определить, какие элементы разрешены.
Контекст. В TaskFlow поле id мы хотим защитить от случайного изменения — помечаем readonly.
readonly — как пломба на счётчике: значение можно прочитать, но «вскрыть» и переписать через этот тип нельзя.const titles: string[] = ["Angular", "React"]; // массив только строк
type Task = {
readonly id: number; // id нельзя переназначить
title: string;
completed: boolean;
};
const task: Task = {
id: 1,
title: "TypeScript",
completed: false
};
// task.id = 2; // ошибка: id защищён
readonly не делает объект глубоко неизменяемым, но защищает конкретное свойство от переназначения через данный тип.
Type alias даёт имя существующему описанию типа.
Контекст. В TaskFlow мы назовём «номер задачи» через TaskId, чтобы не писать number везде.
type TaskId = number; // прозвище для number
type Task = {
id: TaskId; // то же, что id: number
title: string;
completed: boolean;
};
Type aliases особенно удобны, когда тип используется многократно или его смысл становится важной частью архитектуры.
Interface — способ описать форму объекта. В frontend он часто используется для моделей и входных параметров.
Контекст. В TaskFlow interface Task — это «чертёж» задачи, по которому проверяются все данные.
interface Task {
id: number;
title: string;
completed: boolean;
}
function renderTask(task: Task) {
console.log(task.title); // знаем: у task точно есть title
}
TypeScript поддерживает и interfaces, и type aliases. Для большинства простых объектных моделей оба подхода подходят. В дальнейшем курс будет использовать оба, объясняя, почему выбран конкретный вариант.
Union type означает: значение может быть одним из нескольких допустимых типов.
Контекст. В TaskFlow статус задачи — только один из трёх: "todo" | "doing" | "done".
type Status = "todo" | "doing" | "done"; // только эти три значения
let status: Status = "todo";
status = "done";
// status = "cancelled"; // ошибка: такого варианта нет в union
Union types особенно полезны для UI-статусов. Вместо произвольной строки мы ограничиваем возможные состояния. Здесь мы используем строки как типы — это называется literal types и подробно разбирается в следующем разделе.
Literal type ограничивает значение конкретным литералом. Несколько литералов можно объединить в union.
Контекст. В TaskFlow состояние загрузки удобно описать через kind: «loading / success / error».
// предполагается, что тип Task определён в другом файле
type Priority = "low" | "medium" | "high";
type TaskState =
| { kind: "loading" } // грузим
| { kind: "success"; tasks: Task[] } // успех: есть задачи
| { kind: "error"; message: string }; // ошибка: есть текст
Поле kind называется discriminant. По нему TypeScript может сузить тип внутри условия. Такой подход особенно полезен для состояния загрузки, ошибки и успеха.
Знак ? означает, что свойство может отсутствовать.
Контекст. В TaskFlow фильтр может не иметь поискового слова — тогда поле search просто не указано.
? — как необязательная графа в анкете: её можно оставить пустой, и бланк всё равно примут.type Priority = "low" | "medium" | "high";
interface TaskFilter {
search?: string; // может быть, а может не быть
priority?: Priority;
}
Null и undefined — разные состояния. Например, undefined может означать отсутствие свойства или отсутствие присвоенного значения, а null — явно заданное отсутствие значения.
undefined — вы ещё не заполнили графу. null — вы специально написали «здесь пусто».function printOwner(owner: string | null) {
if (owner === null) {
console.log("Без владельца");
return; // выходим, если владельца нет
}
console.log(owner); // здесь owner точно строка
}В TypeScript можно описывать не только данные, но и форму функции.
Контекст. В TaskFlow мы опишем «предикат задачи» — функцию, которая говорит, подходит ли задача.
// предполагается, что тип Task определён в другом файле
type TaskPredicate = (task: Task) => boolean; // берёт Task, возвращает true/false
const isCompleted: TaskPredicate =
task => task.completed; // подходит: берёт задачу, возвращает boolean
Это позволяет передавать функции как значения и сохранять типовой контракт между вызывающим кодом и callback.
Официальная документация TypeScript описывает сигнатуры функций как отдельный типовой механизм.
Generics позволяют написать код, который работает с разными типами, сохраняя связь между входом и выходом.
Представим функцию, которая возвращает первый элемент массива. Нам не хочется терять тип элемента.
Контекст. В TaskFlow first<Task> вернёт именно задачу, а не «что-то непонятное».
function first<T>(items: T[]): T | undefined {
return items[0]; // возвращаем первый элемент (или undefined)
}
const firstTask = first(tasks); // TypeScript знает: Task | undefined
const firstTitle = first(["Angular", "React"]); // знает: string | undefined
В первом случае TypeScript выводит Task | undefined, во втором — string | undefined. Официальный Handbook показывает generics как способ сохранять связь между типами входных и выходных данных.
Generics особенно понятны, когда мы строим маленькие универсальные функции API.
Контекст. В TaskFlow мы сделаем универсальную getJson<T>, которая загружает данные любого типа.
getJson<T> — как универсальная коробка: вы говорите «привезите мне именно Task[]», и на выходе получаете ровно этот тип, а не «коробку непонятно чего».// import type { Task } from "./task";
async function getJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`); // ошибка сети/сервера
}
return await response.json() as T; // говорим TypeScript: результат типа T
}
const tasks = await getJson<Task[]>(
"http://localhost:3000/tasks"
);
Здесь generic говорит вызывающему коду, какой тип результата он ожидает. Но важно: type assertion не проверяет фактический JSON. Это только утверждение для type checker. Поэтому в production при недоверенных данных могут использоваться runtime-схемы или валидаторы.
<T> вы бы написали getJson(url): any и потеряли тип результата. Generic делает ровно обратное: тип, который вы указываете при вызове (getJson<Task[]>), «протягивается» на возвращаемое значение. Входной параметр и выход оказываются соединены одной переменной типа — отсюда и польза автодополнения.▶️ Попробуйте сами: функция first без generic теряет тип элемента. В TypeScript запись first<Task>(tasks) вернула бы именно Task, а не any.
first() возвращает первый элемент массива, и почему без generic TypeScript теряет тип. Меняй код и запускай снова.any практически отключает типовую проверку для значения. unknown наоборот говорит: «мы пока не знаем тип, поэтому сначала проверь». Для внешних данных unknown безопаснее как стартовая точка.
Контекст. В TaskFlow данные с сервера сначала неизвестны — берём unknown, а не any.
any — как «всё равно что внутри», вы открыли дверь и ничего не проверяете. unknown — как «сначала спросим, что внутри, прежде чем трогать».let unsafe: any = "hello";
unsafe.notExisting.method(); // TypeScript не защищает — может упасть при запуске
let external: unknown = "hello";
// external.toUpperCase(); // ошибка: тип неизвестен
if (typeof external === "string") {
external.toUpperCase(); // только после проверки — можно
}
Что увидит пользователь: со строкой после проверки toUpperCase() вернёт HELLO; а any пропустит даже заведомо сломанный вызов.
any как способ убрать ошибку. Сначала выясните, какой тип действительно нужен.Type assertion сообщает TypeScript, что вы как разработчик считаете значение конкретного типа.
Контекст. В TaskFlow иногда мы «знаем лучше компилятора», что значение — строка, и пишем as string.
const value: unknown = "Angular";
const text = value as string; // говорим TypeScript: доверяй, это string
Assertion не изменяет runtime-значение и не проверяет его. Поэтому это инструмент доверия, а не валидации.
as SomeType, скорее всего, ваша модель данных определена неверно. Type assertion не устраняет проблему — он её скрывает.JSON приходит во время выполнения. TypeScript-код не может «превратить» неправильный JSON в правильный только потому, что вы написали интерфейс.
Контекст. В TaskFlow сервер может прислать «кривой» ответ — TypeScript этого до запуска не узнает.
interface Task {
id: number;
title: string;
completed: boolean;
}
const raw: unknown = await response.json(); // что там — не знаем до проверки
Следующий уровень зрелости frontend — runtime validation: проверить, что raw действительно имеет ожидаемую форму. Сегодня нам важно запомнить границу: TypeScript контролирует исходный код, а серверные данные нужно рассматривать как внешние.
raw, проверяем его форму: if (typeof raw === "object" && raw !== null && "title" in raw) { ... }. Позже в курсе познакомимся с библиотеками вроде Zod, которые упрощают runtime-валидацию.Прежде чем писать Angular и React-компоненты, определим единый тип Task. Тогда две реализации приложения будут работать с одной логической моделью.
Контекст. Один общий «чертёж» задачи используется и в Angular, и в React — это связующая нить курса.
export type Priority = "low" | "medium" | "high";
export interface Task {
id: number;
title: string;
description?: string;
completed: boolean;
priority: Priority;
createdAt: string;
}
Такой контракт станет общей нитью всего курса.
Сначала создадим маленькую версию логики без Angular и React. Цель — закрепить язык до перехода к framework.
▶️ Ниже — песочница, где можно запустить весь пример. А пока разберём код по шагам.
tasks.jsКонтекст. Массив задач здесь играет роль «базы данных» в памяти браузера.
// Массив задач — «база данных» в памяти для примера
const tasks = [
{ id: 1, title: "Изучить JS", completed: false }, // completed: false — задача ещё активна
{ id: 2, title: "Изучить TS", completed: true }, // completed: true — задача выполнена
{ id: 3, title: "Создать Angular", completed: false }
];
// items — произвольный массив задач; фильтруем только невыполненные
function getActiveTasks(items) {
// filter оставляет элементы, для которых предикат вернул true
return items.filter(task => !task.completed);
}
// Выводим в консоль результат фильтрации
console.log(getActiveTasks(tasks));Что увидит пользователь: в консоли останутся только невыполненные задачи (id 1 и 3).
toggleTask — как переключатель света: нажал — загорелось, нажал ещё раз — погасло. При этом старый список не переписываем, а делаем новый (map + spread).// items — исходный массив, id — какую задачу переключить
function toggleTask(items, id) {
// map создаёт НОВЫЙ массив, старый не меняем
return items.map(task =>
// если id совпал — инвертируем completed через spread-копию
task.id === id
? { ...task, completed: !task.completed }
: task // иначе возвращаем задачу без изменений
);
}toggleTask. Меняй код и запускай снова.Теперь перенесём модель в TypeScript.
Контекст. Опишем «чертёж» задачи в отдельном файле, чтобы потом переиспользовать в Angular и React.
src/app/models/task.ts (или src/types/task.ts в React)// Union type: приоритет — только одно из трёх допустимых значений
export type Priority = "low" | "medium" | "high";
// Общий контракт задачи для всего курса
export interface Task {
id: number; // уникальный числовой идентификатор
title: string; // заголовок задачи
completed: boolean; // выполнена ли задача (НЕ путать с TaskFilter "done")
priority: Priority; // приоритет из union type выше
}Контекст. Специально напишем «неправильный» приоритет, чтобы увидеть, как TypeScript нас остановит.
// Аннотация типа: ожидаем объект, соответствующий Task
const task: Task = {
id: 1,
title: "TypeScript",
completed: false,
priority: "urgent" // ошибка: "urgent" нет в union Priority
};
Редактор должен сообщить, что urgent не входит в union Priority.
Используем типизированные функции.
Контекст. Напишем три функции для TaskFlow: «только выполненные», «только заголовки», «найти по id» — и укажем их типы.
// import type { Task } from "./task";
// Вход: массив Task. Выход: тоже Task[] (только выполненные)
function getCompleted(tasks: Task[]): Task[] {
return tasks.filter(task => task.completed); // оставляем completed === true
}
// Вход: Task[]. Выход: string[] — массив заголовков
function getTitles(tasks: Task[]): string[] {
return tasks.map(task => task.title); // преобразуем каждую задачу в строку
}
// Вход: массив и id. Выход: Task | undefined — задача может не найтись
function findTask(
tasks: Task[],
id: number
): Task | undefined {
return tasks.find(task => task.id === id); // первый совпавший или undefined
}
Задача студента — объяснить, почему return type указан именно так.
getCompleted, getTitles и findTask на примере задач. Меняй код и запускай снова.Используйте mock API из первого занятия. Если он ещё не запущен:
cd taskflow-api
npm install
npm run api
Контекст. Теперь свяжем нашу модель с реальным сервером: загрузим задачи через fetch.
// import type { Task } from "./task";
// async → функция возвращает Promise; тип результата Promise<Task[]>
async function loadTasks(): Promise<Task[]> {
// await приостанавливает функцию до получения Response
const response = await fetch(
"http://localhost:3000/tasks"
);
// fetch НЕ падает при 404/500, поэтому проверяем сами
if (!response.ok) {
throw new Error(`HTTP ${response.status}`); // явная ошибка → попадёт в catch
}
// await + .json() парсит тело ответа в массив задач
return await response.json();
}
Разбор по шагам:
await fetch).Здесь соединяются сразу несколько тем: function, Promise, generic-like return annotation, fetch, JSON и Task.
Добавим отдельную функцию верхнего уровня.
Контекст. «Обертка» run() ловит любые ошибки загрузки и показывает понятное сообщение.
run() — как дежурный у входа: если курьер не пришёл, дежурный сообщает посетителям, что случилось, вместо того чтобы молча рухнуть.// Верхнеуровневая обёртка: сама ничего не знает про сеть
async function run() {
try {
// здесь выполняется «счастливый» сценарий
const tasks = await loadTasks(); // если loadTasks бросит ошибку — уйдём в catch
console.log("Загружено:", tasks.length);
} catch (error) {
// сюда попадаем при любой ошибке выше (сеть, !ok, JSON)
console.error("Ошибка загрузки", error);
}
}
run(); // запускаем верхнеуровневую функцию
Попробуйте временно изменить порт API с 3000 на 3001. Посмотрите, что произойдёт. Затем верните правильный порт.
fetch и async/await (или сообщение об ошибке). Меняй код и запускай снова.Вы должны уметь ответить: ошибка возникла в TypeScript-коде, в браузере, на HTTP-уровне или потому, что сервер не запущен?
Контекст. В следующих уроках тот же Task будет использовать Angular. Уже сейчас подготовим файл модели, чтобы не переделывать потом.
src/app/models/task.ts и перенесите в него интерфейс Task из раздела 42.src/app/models/task.ts
В Angular файл можно импортировать через обычный ES module import:
import type { Task } from "./models/task";
Это важный момент: язык и типовая модель не являются «собственностью React» или «собственностью Angular». Они общие.
Контекст. В React тип модели тоже хранится отдельно; компонент получает задачу через props.
src/types/task.ts с тем же интерфейсом Task.src/types/task.ts
import type { Task } from "./types/task";
Компонент получает Task через props:
function TaskItem({ task }: { task: Task }) {
return (
<article>
<h2>{task.title}</h2>
</article>
);
}
Что увидит пользователь: на экране появится заголовок задачи внутри карточки article с подзаголовком h2.
Именно поэтому TypeScript удобно изучать до framework: один и тот же язык работает в обеих экосистемах.
Task станет моделью компонента и основой для *ngFor/сигналов, а в Уроке 4 (React) — пропсами TaskItem и элементом useState<Task[]>. Поэтому правильно описанный интерфейс сейчас сэкономит часы позже.Контекст. Закрепите темы урока: типы, приоритеты, обновление задач, generics и загрузку с API. Ниже — задания и песочница, где можно потренироваться.
Добавьте поле assignee, которое может быть строкой или null.
Создайте функцию getHighPriorityTasks.
Создайте функцию renameTask, которая возвращает новую задачу и не изменяет исходную.
Напишите first<T>, возвращающую первый элемент массива или undefined.
Загрузите Task[] из mock API и выведите количество задач с priority === "high".
Сделайте так, чтобы функция загрузки выбрасывала ошибку при статусе 404/500.
Контекст. Если застряли — сверьтесь с образцами. Главное: типы указывают, что ожидается на входе и выходе.
interface Task {
id: number;
title: string;
completed: boolean;
priority: Priority;
assignee: string | null; // добавленное поле
}function getHighPriorityTasks(tasks: Task[]) {
return tasks.filter(task => task.priority === "high");
}function renameTask(task: Task, title: string): Task {
return { ...task, title };
}function first<T>(items: T[]): T | undefined {
return items[0];
}const tasks = await loadTasks();
const high = tasks.filter(task => task.priority === "high");
console.log(high.length);if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}response.ok равно false для всех статусов вне диапазона 200-299, включая 404 и 500.
Совет: сначала сравните идею с вашим решением, а уже потом смотрите код целиком.
const не означает неизменяемый объект?map отличается от filter?response.ok?Краткий справочник терминов урока. Если забыли аналогию — вернитесь к нужному разделу выше.
| Термин | Простое объяснение |
|---|---|
| Variable | Имя, связанное со значением (как ячейка в шкафчике). |
| Scope | Область, в которой имя доступно (как комната). |
| Primitive | Базовый тип значения JavaScript (string, number, boolean…). |
| Callback | Функция, переданная другой функции (как номерок в пиццерии). |
| Closure | Функция, сохраняющая доступ к внешним переменным (блокнот в кармане). |
| Module | Файл с import/export (коробка с инструментами). |
| Promise | Объект, представляющий будущий результат async-операции (обещание + ожидание посылки). |
| async/await | Синтаксис для работы с Promise (обещание + ожидание посылки). |
| Type annotation | Явное указание типа (подписали банку). |
| Inference | Автоматический вывод типа (поняли на вкус). |
| Interface | Описание формы объекта в TypeScript (бланк анкеты). |
| Type alias | Имя для типа (прозвище). |
| Union | Тип, допускающий несколько вариантов (переключатель). |
| Literal type | Тип, допускающий конкретное значение. |
| Generic | Параметризованный тип, сохраняющий связь между типами (универсальная коробка). |
| any | Тип, резко ослабляющий типовую проверку (не проверяем). |
| unknown | Неизвестный тип, который нужно проверить перед использованием. |
| Assertion | Указание TypeScript, какой тип вы предполагаете (наклейка). |
| Runtime | Момент фактического выполнения программы. |
Материал основан на официальной документации и общих механизмах JavaScript/TypeScript.
| Источник | Что читать |
|---|---|
| TypeScript Handbook | Основы языка и типовая система |
| TypeScript Everyday Types | Примитивы, массивы, функции, object types |
| TypeScript Generics | Обобщённые функции и типы |
| MDN Promises | Promise, chaining, async/await, ошибки |
| MDN JavaScript Guide | Основы JavaScript и модули |
Актуальность веб-ссылок и терминологии сверена с официальными материалами на август 2026 года. TypeScript Handbook обновлялся в июле 2026 года. MDN описывает Promise и async/await как фундамент современной асинхронной модели JavaScript.
| Термин | Жизненная аналогия | Коротко |
|---|---|---|
let / const | Ячейка в шкафчике с табличкой-именем | Имя для значения. const нельзя переназначить, let — можно. |
| Массив | Полка с коробками | Упорядоченная коллекция значений (наш список задач). |
filter | Ситечко для чая | Пропускает только подходящие элементы, остальное отбрасывает. |
map | Конвейер (ксерокс) | Каждый элемент проходит операцию и выходит преобразованным. |
reduce | Накопительный итог | Значения по очереди складываются в один общий результат. |
find | Поиск по номеру | Находит один элемент по условию (например, по id). |
| Типы (TypeScript) | Бланк анкеты | Описывают, какие значения допустимы, и подсвечивают ошибки до запуска. |
| Интерфейс | Бланк анкеты (чертёж) | Форма объекта: какие поля обязательны, а какие нет. |
Promise / async-await | Обещание + ожидание посылки | Результат придёт позже — либо успех, либо ошибка. |
| Generics | Универсальная коробка | Параметр типа «протягивается» сквозь функцию (например, first<Task>). |
1. let означает, что значение переменной вообще нельзя изменить. Верно или неверно?
Ответ: Неверно. Менять значение можно именно у let; у const — нельзя. Как ячейка в шкафчике: let — та, которую можно «переклеить» на другой шкаф, а const — заклеена намертво.
2. map оставляет только подходящие элементы, а filter преобразует каждый элемент. Верно или неверно?
Ответ: Неверно — ровно наоборот. filter — как ситечко: отбирает нужное. map — как конвейер: берёт каждый элемент, преобразует и возвращает новый такого же размера.
3. Promise — это обещание получить результат позже, как посылку. Верно или неверно?
Ответ: Верно. Promise — это обещание + ожидание посылки: вы сразу получаете «талончик», а данные придут позже — либо успешно (тогда сработает .then), либо с ошибкой (.catch).