Хотите заказать веб-сайт? Связаться с нами

Практическая типизация в TypeScript. Примеры

Теория — это хорошо, но настоящие навыки приходят только с практикой. Сегодня мы полностью посвятим урок практическому упражнению. Я подготовил для вас реальный JavaScript-код со множеством скрытых проблем. Ваша задача — превратить его в TypeScript, добавив все необходимые аннотации типов, исправив ошибки и сделав код безопасным.

Это упражнение объединит всё, что мы изучили за предыдущие уроки:

  1. Примитивные и литеральные типы
  2. Массивы и объекты
  3. Тип-объединение (Union Types)
  4. Защитники типа (Type Guards)
  5. Псевдонимы типа (Type Aliases)
  6. Работу с опциональными свойствами
  7. Вывод типов (type inference)
Практическая типизация в TypeScript. Примеры

Подготовка

Создайте новый файл exercise.ts и скопируйте в него следующий код. Мы будем работать с ним на протяжении всего урока.

// exercise.ts — Исходный JavaScript код
// Здесь много скрытых проблем. Ваша задача — добавить типы!

// 1. Простые переменные
const appName = "Task Manager";
const version = 1.0;
const isProduction = false;
const config = null;

// 2. Функция для создания задач
function createTask(title, description, priority, dueDate) {
  return {
    id: Date.now(),
    title: title,
    description: description,
    priority: priority,
    dueDate: dueDate,
    completed: false,
    tags: []
  };
}

// 3. Функция для обновления задачи
function updateTask(task, updates) {
  return { ...task, ...updates };
}

// 4. Функция для фильтрации задач
function filterTasks(tasks, filterType) {
  if (filterType === "completed") {
    return tasks.filter(task => task.completed === true);
  } else if (filterType === "pending") {
    return tasks.filter(task => task.completed === false);
  } else if (filterType === "high") {
    return tasks.filter(task => task.priority === "high");
  } else {
    return tasks;
  }
}

// 5. Функция для форматирования задачи
function formatTask(task) {
  const status = task.completed ? "✅" : "⏳";
  const priorityEmoji = task.priority === "high" ? "🔴" : 
                        task.priority === "medium" ? "🟡" : "🟢";
  return `${status} ${priorityEmoji} ${task.title} (${task.dueDate})`;
}

// 6. Функция для подсчёта статистики
function getTaskStats(tasks) {
  const total = tasks.length;
  const completed = tasks.filter(t => t.completed).length;
  const pending = total - completed;
  const highPriority = tasks.filter(t => t.priority === "high").length;
  
  return {
    total,
    completed,
    pending,
    highPriority
  };
}

// 7. Создаём несколько задач
const task1 = createTask(
  "Изучить TypeScript",
  "Пройти уроки по TypeScript и выполнить упражнения",
  "high",
  "2024-01-30"
);

const task2 = createTask(
  "Купить продукты",
  "Молоко, хлеб, яйца",
  "low",
  null
);

const task3 = createTask(
  "Написать отчёт",
  "Подготовить еженедельный отчёт для команды",
  "medium",
  "2024-01-25"
);

// 8. Выполняем операции
const tasks = [task1, task2, task3];

console.log("Все задачи:");
tasks.forEach(task => console.log(formatTask(task)));

console.log("\nСтатистика:");
console.log(getTaskStats(tasks));

console.log("\nТолько высокий приоритет:");
const highTasks = filterTasks(tasks, "high");
highTasks.forEach(task => console.log(formatTask(task)));

// 9. Обновляем задачу
const updatedTask = updateTask(task1, { completed: true, priority: "medium" });
console.log("\nОбновлённая задача:");
console.log(formatTask(updatedTask));

Ваше задание

Теперь ваша очередь. Выполните следующие шаги:

  1. Добавьте типы для всех переменных — используйте явные аннотации или позвольте TypeScript вывести их.
  2. Создайте Type Aliases для повторяющихся структур (Task, TaskPriority, FilterType).
  3. Добавьте аннотации для всех параметров функций — укажите, какие типы принимают функции.
  4. Добавьте аннотации для возвращаемых значений — укажите, что возвращает каждая функция.
  5. Используйте Union Types для параметров, которые могут быть разных типов.
  6. Используйте защитник типа для проверки на null и undefined.
  7. Исправьте ошибки — TypeScript укажет на проблемные места.

Попробуйте сделать это самостоятельно. Не читайте решение, пока не попытаетесь решить задачу сами!

Пошаговый разбор решения

Давайте теперь пройдём по всем проблемам и исправим их.

Шаг 1: Типы для переменных

Начнём с простых переменных. Здесь TypeScript может вывести типы автоматически:

// TypeScript выведет типы самостоятельно
const appName = "Task Manager"; // string
const version = 1.0; // number
const isProduction = false; // boolean
const config = null; // null

Но если мы хотим быть явными, можем добавить аннотации:

const appName: string = "Task Manager";
const version: number = 1.0;
const isProduction: boolean = false;
const config: null = null;

Шаг 2: Type Aliases для Task

Структура задачи используется во многих местах, поэтому создадим псевдонимы типов:

// Приоритеты задач
type TaskPriority = "low" | "medium" | "high";

// Тип задачи
type Task = {
  id: number;
  title: string;
  description: string;
  priority: TaskPriority;
  dueDate: string | null; // может быть null
  completed: boolean;
  tags: string[];
};

// Тип для обновлений задачи (все поля опциональны)
type TaskUpdate = Partial<Task>;

// Тип для фильтрации
type FilterType = "completed" | "pending" | "high" | "all";

Шаг 3: Функция createTask

Добавляем типы для параметров и возвращаемого значения. Обратите внимание на dueDate — он может быть null:

function createTask(
  title: string,
  description: string,
  priority: TaskPriority,
  dueDate: string | null
): Task {
  return {
    id: Date.now(),
    title: title,
    description: description,
    priority: priority,
    dueDate: dueDate,
    completed: false,
    tags: []
  };
}

Шаг 4: Функция updateTask

Используем Partial<Task> для параметра updates:

function updateTask(task: Task, updates: Partial<Task>): Task {
  return { ...task, ...updates };
}

Шаг 5: Функция filterTasks

Здесь важно использовать Type Guard для проверки типа filterType:

function filterTasks(tasks: Task[], filterType: FilterType): Task[] {
  // Используем switch для более чистого кода
  switch (filterType) {
    case "completed":
      return tasks.filter(task => task.completed === true);
    case "pending":
      return tasks.filter(task => task.completed === false);
    case "high":
      return tasks.filter(task => task.priority === "high");
    case "all":
    default:
      return tasks;
  }
}

Шаг 6: Функция formatTask

Здесь нужно добавить проверку на null для dueDate:

function formatTask(task: Task): string {
  const status = task.completed ? "✅" : "⏳";
  const priorityEmoji = task.priority === "high" ? "🔴" : 
                        task.priority === "medium" ? "🟡" : "🟢";
  
  // Обрабатываем случай, когда dueDate === null
  const dueDateStr = task.dueDate ?? "No due date";
  
  return `${status} ${priorityEmoji} ${task.title} (${dueDateStr})`;
}

Шаг 7: Функция getTaskStats

Типизируем возвращаемое значение:

type TaskStats = {
  total: number;
  completed: number;
  pending: number;
  highPriority: number;
};

function getTaskStats(tasks: Task[]): TaskStats {
  const total = tasks.length;
  const completed = tasks.filter(t => t.completed).length;
  const pending = total - completed;
  const highPriority = tasks.filter(t => t.priority === "high").length;
  
  return {
    total,
    completed,
    pending,
    highPriority
  };
}

Шаг 8: Улучшаем типизацию с помощью Type Inference

Заметим, что TypeScript может вывести тип tasks автоматически, но лучше указать его явно:

// TypeScript выведет Task[]
const tasks = [task1, task2, task3];

// Или явно указываем
const tasks: Task[] = [task1, task2, task3];

Шаг 9: Финальный код

Вот как выглядит итоговый код после всех исправлений:

// ============================================
// 1. Type Aliases
// ============================================

type TaskPriority = "low" | "medium" | "high";

type Task = {
  id: number;
  title: string;
  description: string;
  priority: TaskPriority;
  dueDate: string | null;
  completed: boolean;
  tags: string[];
};

type TaskUpdate = Partial<Task>;
type FilterType = "completed" | "pending" | "high" | "all";

type TaskStats = {
  total: number;
  completed: number;
  pending: number;
  highPriority: number;
};

// ============================================
// 2. Переменные
// ============================================

const appName: string = "Task Manager";
const version: number = 1.0;
const isProduction: boolean = false;
const config: null = null;

// ============================================
// 3. Функции
// ============================================

function createTask(
  title: string,
  description: string,
  priority: TaskPriority,
  dueDate: string | null
): Task {
  return {
    id: Date.now(),
    title: title,
    description: description,
    priority: priority,
    dueDate: dueDate,
    completed: false,
    tags: []
  };
}

function updateTask(task: Task, updates: TaskUpdate): Task {
  return { ...task, ...updates };
}

function filterTasks(tasks: Task[], filterType: FilterType): Task[] {
  switch (filterType) {
    case "completed":
      return tasks.filter(task => task.completed === true);
    case "pending":
      return tasks.filter(task => task.completed === false);
    case "high":
      return tasks.filter(task => task.priority === "high");
    case "all":
    default:
      return tasks;
  }
}

function formatTask(task: Task): string {
  const status = task.completed ? "✅" : "⏳";
  const priorityEmoji = task.priority === "high" ? "🔴" : 
                        task.priority === "medium" ? "🟡" : "🟢";
  const dueDateStr = task.dueDate ?? "No due date";
  
  return `${status} ${priorityEmoji} ${task.title} (${dueDateStr})`;
}

function getTaskStats(tasks: Task[]): TaskStats {
  const total = tasks.length;
  const completed = tasks.filter(t => t.completed).length;
  const pending = total - completed;
  const highPriority = tasks.filter(t => t.priority === "high").length;
  
  return {
    total,
    completed,
    pending,
    highPriority
  };
}

// ============================================
// 4. Использование
// ============================================

const task1: Task = createTask(
  "Изучить TypeScript",
  "Пройти уроки по TypeScript и выполнить упражнения",
  "high",
  "2024-01-30"
);

const task2: Task = createTask(
  "Купить продукты",
  "Молоко, хлеб, яйца",
  "low",
  null
);

const task3: Task = createTask(
  "Написать отчёт",
  "Подготовить еженедельный отчёт для команды",
  "medium",
  "2024-01-25"
);

const tasks: Task[] = [task1, task2, task3];

console.log("Все задачи:");
tasks.forEach(task => console.log(formatTask(task)));

console.log("\nСтатистика:");
console.log(getTaskStats(tasks));

console.log("\nТолько высокий приоритет:");
const highTasks = filterTasks(tasks, "high");
highTasks.forEach(task => console.log(formatTask(task)));

const updatedTask = updateTask(task1, { completed: true, priority: "medium" });
console.log("\nОбновлённая задача:");
console.log(formatTask(updatedTask));

Что мы улучшили?

  1. Безопасность — теперь ни одна функция не может быть вызвана с неправильным типом.
  2. Читаемость — псевдонимы типов делают код самодокументируемым.
  3. Поддержка null — мы явно обработали случай, когда dueDate равен null.
  4. Переиспользуемость — типы можно экспортировать и использовать в других файлах.
  5. Рефакторинг — использование switch вместо if/else сделало код чище.
Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

Запуск и проверка

Чтобы проверить ваше решение, запустите компилятор:

tsc exercise.ts --noEmit --strict

Если ошибок нет — поздравляю, вы успешно прошли упражнение!

Альтернативно, вы можете запустить код с помощью TSX:

tsx exercise.ts

Бонус: дополнительные улучшения

Если вы хотите пойти дальше, вот несколько идей для улучшения:

  1. Добавьте валидацию — используйте защитник типа для проверки, что title не пустая строка.
  2. Создайте фабрику задач — напишите функцию, которая генерирует задачи с случайными данными.
  3. Добавьте метод для добавления тегов — расширьте функциональность работы с тегами.
  4. Используйте utility types — попробуйте использовать Pick, Omit, Readonly для разных сценариев.

Итог

Сегодня мы на практике применили все знания, полученные за предыдущие уроки:

  1. Примитивные и литеральные типы для переменных и параметров.
  2. Псевдонимы для переиспользуемых структур данных.
  3. Объединение для параметров, которые могут быть разных типов.
  4. Защитники для безопасной работы с null и undefined.
  5. Аннотации типов для всех функций и их параметров.
  6. Утилитарные типы (Partial) для более гибкой типизации.

Это упражнение — хороший пример того, как выглядит реальная миграция кода на TypeScript. В следующем уроке мы перейдём к более техническим аспектам — научимся компилировать TypeScript-файлы и разберёмся с инструментами TSC и TSX.

Теги: