Итоговый практический проект в TypeScript: подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты учишься готовить. Сначала ты учишься резать овощи (переменные), потом жарить яйца (функции), затем смешивать ингредиенты (объекты). Но чтобы приготовить настоящий ужин для друзей, тебе нужно объединить все эти навыки в одно целое блюдо. Вот это и есть итоговый практический проект — твой собственный «ужин» из кода.
В реальной жизни программисты не пишут отдельные кусочки кода ради забавы. Они создают программы, которые решают настоящие проблемы: сайты для покупок, игры, приложения для заметок, калькуляторы. Итоговый проект — это мостик между учебой и реальной работой. Ты берешь все, что выучил: типы данных, функции, классы, массивы, циклы — и собираешь из этого что-то работающее.
💡 Важно: Итоговый проект не обязан быть огромным. Даже простая программа, которая работает без ошибок и делает что-то полезное — это уже победа. Главное — ты сам её написал от начала до конца.
Зачем это нужно? Во-первых, чтобы закрепить знания. Когда ты просто читаешь теорию, кажется, что всё понятно. Но как только садишься писать код — начинаются вопросы: «А как это соединить?», «А почему ошибка?». Проект заставляет твой мозг работать по-настоящему. Во-вторых, это твоё портфолио. Когда ты будешь искать работу или показывать друзьям, что умеешь, ты скажешь: «Вот, я написал это». Работодатели любят смотреть на реальные проекты, а не на оценки в школе.
В этой статье мы разберём, как спроектировать и написать итоговый проект на TypeScript. Мы не будем делать что-то скучное вроде «Hello World». Мы создадим менеджер задач (Todo List) — программу, которая умеет добавлять, удалять, отмечать выполненные задачи и сохранять их. Это классика, но на её примере ты поймёшь все принципы построения настоящих приложений.
Теория: как это работает изнутри
Любая программа, даже самая сложная, состоит из трёх частей: вход (что мы даём программе), обработка (что программа делает с данными) и выход (что мы получаем). В нашем менеджере задач это выглядит так:
- Вход: пользователь вводит текст задачи, нажимает кнопку «Добавить» или «Удалить».
- Обработка: программа сохраняет задачу в список, меняет её статус или удаляет.
- Выход: программа показывает обновлённый список задач на экране.
Но как это устроено внутри? Давай заглянем под капот. В TypeScript (и в JavaScript, на котором он основан) есть несколько ключевых понятий, которые мы будем использовать:
- Типы и интерфейсы. TypeScript добавляет строгую типизацию. Это значит, что мы заранее говорим: «Задача — это объект, у которого есть текст (строка) и статус (выполнена или нет)». Если кто-то попытается записать в задачу число вместо текста — TypeScript сразу укажет на ошибку. Это как табличка на двери: «Сюда входить только с тапками».
- Классы. Класс — это чертёж, по которому создаются объекты. Например, у нас будет класс
TaskManager, который отвечает за все операции с задачами. Внутри класса есть методы (функции) —addTask,deleteTask,toggleTask. - Массивы и методы массивов. Все задачи хранятся в массиве. Чтобы добавить задачу — используем
push, чтобы удалить —filter, чтобы найти —findилиfindIndex. - Работа с DOM. Если мы пишем проект для браузера, нам нужно взаимодействовать с HTML-страницей. Мы будем получать элементы со страницы (кнопки, поля ввода, списки) и менять их содержимое.
- Сохранение данных. Если просто хранить задачи в массиве, при обновлении страницы они исчезнут. Чтобы этого не случилось, мы используем
localStorage— специальное хранилище в браузере, которое помнит данные даже после закрытия вкладки.
⚠️ Важное замечание: TypeScript — это не отдельный язык, а надстройка над JavaScript. Весь код на TypeScript в итоге компилируется (превращается) в обычный JavaScript, который понимает браузер. Но писать на TypeScript удобнее, потому что он ловит ошибки ещё до того, как программа запустится.
Теперь давай представим архитектуру нашего проекта. У нас будет:
- Модель (Model): описание задачи — интерфейс
Task. - Хранилище (Store): класс
TaskStore, который управляет массивом задач и сохраняет их вlocalStorage. - Представление (View): класс
TaskView, который отвечает за отрисовку задач на странице. - Контроллер (Controller): класс
TaskController, который связывает хранилище и представление, обрабатывает действия пользователя.
Это называется MVC-архитектура (Model-View-Controller). Не пугайся названия — это просто способ организовать код, чтобы он не превратился в кашу. Каждый класс делает только свою работу.
Синтаксис в TypeScript: все формы и варианты
Давай разберём ключевые конструкции TypeScript, которые нам понадобятся. Я покажу их на примерах, а потом мы соберём всё вместе.
1. Интерфейсы
Интерфейс — это описание формы объекта. Мы говорим: «У задачи должно быть поле id (число), title (строка) и completed (булево значение — true/false)».
// Описываем, как выглядит задача
interface Task {
id: number; // уникальный номер задачи
title: string; // текст задачи
completed: boolean; // выполнена или нет
}
2. Типы (type)
Кроме интерфейсов, можно использовать type. Разница небольшая, но для простых проектов подойдёт и то, и другое.
// Альтернативный способ описать задачу
type Task = {
id: number;
title: string;
completed: boolean;
};
3. Классы
Класс — это как завод по производству объектов. У класса есть свойства (переменные внутри) и методы (функции внутри).
// Класс для управления задачами
class TaskManager {
// Свойство: массив задач, изначально пустой
private tasks: Task[] = [];
// Метод: добавить задачу
addTask(title: string): void {
// Создаём новую задачу
const newTask: Task = {
id: Date.now(), // уникальный id на основе текущего времени
title: title,
completed: false
};
// Добавляем в массив
this.tasks.push(newTask);
}
}
🔑 Ключевой момент:privateозначает, что свойствоtasksдоступно только внутри класса. Снаружи его нельзя изменить напрямую — только через методы. Это защищает данные от случайных ошибок.
4. Дженерики (Generics)
Дженерики — это способ сделать код гибким. Например, мы хотим написать функцию, которая работает с любым типом данных, а не только с задачами.
// Функция, которая находит элемент по id. T — это любой тип.
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// Используем с задачами
const task = findById(tasks, 1); // TypeScript сам понимает, что task — это Task
5. Union и Intersection типы
Иногда переменная может быть одного из нескольких типов. Это называется Union (объединение).
// Статус задачи может быть строкой или булевым значением
type Status = string | boolean;
// А Intersection (пересечение) объединяет два типа в один
type NamedTask = Task & { owner: string }; // у задачи теперь есть ещё и владелец
6. Enum (перечисления)
Если у нас есть фиксированный набор значений, удобно использовать Enum.
// Перечисление статусов задачи
enum TaskStatus {
Active = 'active',
Completed = 'completed',
Archived = 'archived'
}
// Теперь можно использовать TaskStatus.Active вместо строки 'active'
const currentStatus: TaskStatus = TaskStatus.Active;
7. Модификаторы доступа
В TypeScript есть три модификатора: public (доступно всем), private (только внутри класса), protected (внутри класса и в наследниках).
class BaseManager {
public name: string; // можно читать и менять откуда угодно
private secret: string; // только внутри этого класса
protected config: object; // внутри класса и у наследников
}
Примеры кода — от простого к сложному
Теперь самое интересное. Мы будем постепенно строить наш менеджер задач. Каждый пример — это шаг вперёд. Внимательно читай комментарии — они объясняют каждую строчку.
Пример 1: Создаём интерфейс задачи и базовый класс
// Шаг 1: Описываем структуру задачи
interface Task {
id: number;
title: string;
completed: boolean;
}
// Шаг 2: Создаём класс-менеджер
class TaskManager {
// Приватное свойство: массив задач
private tasks: Task[] = [];
// Метод для добавления задачи
addTask(title: string): void {
// Создаём объект задачи
const newTask: Task = {
id: Date.now(), // уникальный id (число миллисекунд с 1970 года)
title: title, // текст задачи
completed: false // по умолчанию не выполнена
};
// Добавляем в конец массива
this.tasks.push(newTask);
console.log(`Задача "${title}" добавлена!`);
}
// Метод для получения всех задач
getTasks(): Task[] {
return this.tasks;
}
}
// Шаг 3: Проверяем работу
const manager = new TaskManager();
manager.addTask('Купить молоко');
manager.addTask('Сделать уроки');
console.log(manager.getTasks());
// Вывод: [ { id: 1234567890, title: 'Купить молоко', completed: false }, ... ]
Пример 2: Удаление задачи по id
// Добавляем метод удаления в класс TaskManager
class TaskManager {
private tasks: Task[] = [];
addTask(title: string): void {
const newTask: Task = {
id: Date.now(),
title: title,
completed: false
};
this.tasks.push(newTask);
}
// Метод удаления: принимает id задачи
deleteTask(id: number): void {
// filter создаёт новый массив, оставляя только те задачи,
// у которых id НЕ равен переданному
this.tasks = this.tasks.filter(task => task.id !== id);
console.log(`Задача с id ${id} удалена`);
}
getTasks(): Task[] {
return this.tasks;
}
}
// Проверяем
const manager = new TaskManager();
manager.addTask('Задача 1');
manager.addTask('Задача 2');
console.log('До удаления:', manager.getTasks().length); // 2
manager.deleteTask(manager.getTasks()[0].id); // удаляем первую
console.log('После удаления:', manager.getTasks().length); // 1
Пример 3: Отметка задачи как выполненной
// Метод для переключения статуса задачи
toggleTask(id: number): void {
// Ищем задачу по id
const task = this.tasks.find(t => t.id === id);
if (task) {
// Если нашли — меняем статус на противоположный
task.completed = !task.completed;
console.log(`Задача "${task.title}" теперь ${task.completed ? 'выполнена' : 'не выполнена'}`);
} else {
console.log('Задача не найдена');
}
}
// Полный пример использования
const manager = new TaskManager();
manager.addTask('Полить цветы');
const taskId = manager.getTasks()[0].id;
manager.toggleTask(taskId); // теперь выполнена
manager.toggleTask(taskId); // снова не выполнена
Пример 4: Сохранение в localStorage
// Добавляем сохранение данных в браузере
class TaskManager {
private tasks: Task[] = [];
private storageKey: string = 'tasks'; // ключ для localStorage
constructor() {
// При создании менеджера загружаем сохранённые задачи
this.loadTasks();
}
// Загрузка из localStorage
private loadTasks(): void {
const data = localStorage.getItem(this.storageKey);
if (data) {
// JSON.parse превращает строку обратно в массив
this.tasks = JSON.parse(data);
}
}
// Сохранение в localStorage
private saveTasks(): void {
// JSON.stringify превращает массив в строку
localStorage.setItem(this.storageKey, JSON.stringify(this.tasks));
}
addTask(title: string): void {
const newTask: Task = {
id: Date.now(),
title: title,
completed: false
};
this.tasks.push(newTask);
this.saveTasks(); // сохраняем после каждого изменения
}
deleteTask(id: number): void {
this.tasks = this.tasks.filter(task => task.id !== id);
this.saveTasks();
}
toggleTask(id: number): void {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
this.saveTasks();
}
}
getTasks(): Task[] {
return this.tasks;
}
}
// Теперь задачи не пропадают при обновлении страницы!
Пример 5: Работа с DOM — отображаем задачи на странице
// Предположим, у нас есть HTML с элементами:
//
//
//
class TaskView {
private listElement: HTMLUListElement;
private inputElement: HTMLInputElement;
private addButton: HTMLButtonElement;
private manager: TaskManager;
constructor(manager: TaskManager) {
this.manager = manager;
// Получаем элементы со страницы
this.listElement = document.getElementById('taskList') as HTMLUListElement;
this.inputElement = document.getElementById('taskInput') as HTMLInputElement;
this.addButton = document.getElementById('addButton') as HTMLButtonElement;
// Вешаем обработчик на кнопку
this.addButton.addEventListener('click', () => this.handleAdd());
}
// Обработчик добавления задачи
private handleAdd(): void {
const title = this.inputElement.value.trim();
if (title) {
this.manager.addTask(title);
this.inputElement.value = ''; // очищаем поле ввода
this.render(); // обновляем список на экране
}
}
// Отрисовка всех задач
render(): void {
this.listElement.innerHTML = ''; // очищаем список
const tasks = this.manager.getTasks();
tasks.forEach(task => {
const li = document.createElement('li');
li.textContent = task.title;
// Если задача выполнена — перечёркиваем
if (task.completed) {
li.style.textDecoration = 'line-through';
}
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Удалить';
deleteBtn.addEventListener('click', () => {
this.manager.deleteTask(task.id);
this.render();
});
// Кнопка переключения статуса
const toggleBtn = document.createElement('button');
toggleBtn.textContent = task.completed ? 'Отменить' : 'Выполнено';
toggleBtn.addEventListener('click', () => {
this.manager.toggleTask(task.id);
this.render();
});
li.appendChild(toggleBtn);
li.appendChild(deleteBtn);
this.listElement.appendChild(li);
});
}
}
// Запуск приложения
const manager = new TaskManager();
const view = new TaskView(manager);
view.render(); // показываем задачи при загрузке
Пример 6: Фильтрация задач (все / активные / выполненные)
// Добавляем фильтрацию в TaskManager
class TaskManager {
// ... (все предыдущие методы)
// Метод для получения отфильтрованных задач
getFilteredTasks(filter: 'all' | 'active' | 'completed'): Task[] {
switch (filter) {
case 'active':
return this.tasks.filter(t => !t.completed);
case 'completed':
return this.tasks.filter(t => t.completed);
default:
return this.tasks;
}
}
}
// В TaskView добавляем кнопки фильтрации
class TaskView {
private filterButtons: NodeListOf;
private currentFilter: 'all' | 'active' | 'completed' = 'all';
constructor(manager: TaskManager) {
// ... (предыдущий код)
this.filterButtons = document.querySelectorAll('.filter-btn');
this.filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
this.currentFilter = btn.dataset.filter as 'all' | 'active' | 'completed';
this.render();
});
});
}
render(): void {
this.listElement.innerHTML = '';
// Используем фильтр
const tasks = this.manager.getFilteredTasks(this.currentFilter);
tasks.forEach(task => {
// ... (отрисовка как в примере 5)
});
}
}
Пример 7: Полный проект с использованием модулей
// Файл: types.ts
export interface Task {
id: number;
title: string;
completed: boolean;
}
export type FilterType = 'all' | 'active' | 'completed';
// Файл: TaskManager.ts
import { Task, FilterType } from './types.js';
export class TaskManager {
private tasks: Task[] = [];
private storageKey: string = 'tasks';
constructor() {
this.loadTasks();
}
private loadTasks(): void {
const data = localStorage.getItem(this.storageKey);
if (data) {
this.tasks = JSON.parse(data);
}
}
private saveTasks(): void {
localStorage.setItem(this.storageKey, JSON.stringify(this.tasks));
}
addTask(title: string): void {
const newTask: Task = {
id: Date.now(),
title: title,
completed: false
};
this.tasks.push(newTask);
this.saveTasks();
}
deleteTask(id: number): void {
this.tasks = this.tasks.filter(task => task.id !== id);
this.saveTasks();
}
toggleTask(id: number): void {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
this.saveTasks();
}
}
getFilteredTasks(filter: FilterType): Task[] {
switch (filter) {
case 'active': return this.tasks.filter(t => !t.completed);
case 'completed': return this.tasks.filter(t => t.completed);
default: return this.tasks;
}
}
getTasks(): Task[] {
return this.tasks;
}
}
// Файл: main.ts
import { TaskManager } from './TaskManager.js';
import { FilterType } from './types.js';
class App {
private manager: TaskManager;
private listElement: HTMLUListElement;
private inputElement: HTMLInputElement;
private currentFilter: FilterType = 'all';
constructor() {
this.manager = new TaskManager();
this.listElement = document.getElementById('taskList') as HTMLUListElement;
this.inputElement = document.getElementById('taskInput') as HTMLInputElement;
document.getElementById('addButton')!.addEventListener('click', () => this.addTask());
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.currentFilter = (e.target as HTMLButtonElement).dataset.filter as FilterType;
this.render();
});
});
this.render();
}
private addTask(): void {
const title = this.inputElement.value.trim();
if (title) {
this.manager.addTask(title);
this.inputElement.value = '';
this.render();
}
}
private render(): void {
this.listElement.innerHTML = '';
const tasks = this.manager.getFilteredTasks(this.currentFilter);
tasks.forEach(task => {
const li = document.createElement('li');
li.textContent = task.title;
if (task.completed) li.style.textDecoration = 'line-through';
const toggleBtn = document.createElement('button');
toggleBtn.textContent = task.completed ? 'Отменить' : 'Выполнено';
toggleBtn.addEventListener('click', () => {
this.manager.toggleTask(task.id);
this.render();
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Удалить';
deleteBtn.addEventListener('click', () => {
this.manager.deleteTask(task.id);
this.render();
});
li.appendChild(toggleBtn);
li.appendChild(deleteBtn);
this.listElement.appendChild(li);
});
}
}
new App();
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет проще понять TypeScript. Давай сравним ключевые моменты.
| Характеристика | TypeScript | Python |
|---|---|---|
| Типизация | Строгая, статическая (типы указываются явно) | Динамическая (тип определяется сам) |
| Объявление переменной | let name: string = 'Анна'; |
name = 'Анна' |
| Функции | function add(a: number, b: number): number { return a + b; } |
def add(a, b): return a + b |
| Классы | Есть, с модификаторами доступа | Есть, но все свойства публичные |
| Массивы | let arr: number[] = [1, 2, 3]; |
arr = [1, 2, 3] |
| Словари / Объекты | interface или type для описания формы |
dict — просто ключи и значения |
| Обработка ошибок | try { ... } catch (error) { ... } |
try: ... except Exception as e: ... |
| Работа с файлами | Через браузерный localStorage или fetch |
open(), read(), write() |
| Среда выполнения | Браузер или Node.js | Интерпретатор Python |
📌 Главное отличие: TypeScript заставляет тебя думать о типах заранее. Это кажется сложным сначала, но спасает от многих ошибок. Python даёт больше свободы, но и ошибки может пропустить. Выбор зависит от задачи: для веб-сайтов лучше TypeScript, для анализа данных — Python.
Типичные ошибки и подводные камни
Даже опытные программисты иногда спотыкаются на ровном месте. Вот список самых частых ошибок в проектах на TypeScript и как их избежать.
- Забыл указать тип. Если ты пишешь
let x = 5;— TypeScript сам догадается, что это число. Но если ты напишешьlet x;и потом присвоишьx = 'строка', а потомx = 5— TypeScript будет ругаться, потому что переменная не может менять тип. Всегда указывай тип, если сомневаешься. - Путаница между
nullиundefined. В TypeScript есть оба значения.null— это «ничего» (ты сам так задал),undefined— «значение ещё не присвоено». Используй??(оператор нулевого слияния) для безопасной проверки:const name = user.name ?? 'Гость'; - Забыл про
async/awaitпри работе с localStorage. На самом деле localStorage работает синхронно, но если ты будешь работать с сервером (черезfetch), безasync/awaitне обойтись. Всегда обрабатывай промисы. - Мутация массива вместо создания нового. Методы
push,splice,sortизменяют исходный массив. Если ты не хочешь менять оригинал, используйconcat,slice,toSorted(новый метод). - Неправильное приведение типов. Когда ты получаешь элемент со страницы через
document.getElementById, TypeScript не знает, какой это элемент. Используйas HTMLInputElementили проверяй черезinstanceof. - Забыл сохранить в localStorage после изменения. Если ты добавил задачу, но не вызвал
saveTasks(), при обновлении страницы всё пропадёт. Всегда синхронизируй память и хранилище. - Игнорирование строгого режима. Включи
"strict": trueвtsconfig.json. Это заставит тебя писать более качественный код и ловить ошибки на этапе компиляции.
Лучшие практики и советы профессионалов
Хочешь писать код, который не стыдно показать на собеседовании? Следуй этим правилам.
- Разделяй код на модули. Один файл — одна ответственность. Интерфейсы в
types.ts, логика вmanager.ts, отрисовка вview.ts. Это называется «принцип единственной ответственности» (Single Responsibility Principle). - Используй строгую типизацию везде. Даже если TypeScript может сам догадаться, явно указывай типы возвращаемых значений функций. Это делает код самодокументируемым.
- Пиши комментарии только там, где это действительно нужно. Комментарий «прибавляем единицу» к строке
i++— бесполезен. Комментарий «используем хэш-таблицу для быстрого поиска» — полезен. - Не бойся рефакторинга. Если код стал слишком большим — разбей его. Если метод делает слишком много — раздели на несколько. Хороший код — это код, который легко читать и менять.
- Тестируй свой код. Напиши хотя бы пару простых тестов. Например, проверь, что после добавления задачи их стало на одну больше. TypeScript с этим помогает, но тесты ловят логические ошибки.
- Используй линтер и форматтер. ESLint и Prettier сделают твой код единообразным. Это как орфография в тексте — мелочь, но важная.
- Не оптимизируй преждевременно. Сначала напиши работающий код, потом — быстрый. 90% времени программа тратит на 10% кода. Найди эти 10% и оптимизируй их.
Вопросы для самопроверки
- Что такое интерфейс в TypeScript и зачем он нужен?
Ответ: Интерфейс — это описание структуры объекта. Он нужен, чтобы гарантировать, что объект имеет определённые поля с определёнными типами. Это помогает ловить ошибки на этапе написания кода. - Чем отличается
privateотpublicв классе?
Ответ:private— свойство или метод доступны только внутри класса.public— доступны отовсюду. По умолчанию всёpublic. - Как сохранить данные в браузере, чтобы они не пропадали при обновлении страницы?
Ответ: ИспользоватьlocalStorage. Данные хранятся в виде строк, поэтому объекты нужно превращать в строку черезJSON.stringify(), а при загрузке — обратно черезJSON.parse(). - Что такое