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

Ошибки компилятора TypeScript: упражнение

В предыдущих уроках мы разобрали, как компилятор TypeScript помогает ловить ошибки ещё до запуска кода, и как флаги вроде --noEmit встраиваются в рабочий процесс разработки. Теперь пришло время закрепить это на практике: перед вами небольшой файл с профилями пользователей, в котором компилятор находит сразу несколько проблем. Ваша задача — не меняя логику программы, объявить недостающие типы так, чтобы tsc перестал ругаться.

Ошибки компилятора TypeScript: упражнение

Что нужно сделать в упражнении

Представьте, что вам достался небольшой модуль интернет-магазина, отвечающий за работу с профилями покупателей. Часть функций в этом модуле не имеет явных типов параметров, а одна из функций должна возвращать «публичную» версию профиля без чувствительных данных. При запуске компилятора в строгом режиме (strict) все эти места подсвечиваются как ошибки, потому что TypeScript не может понять, с какими значениями работает код.

Файл с ошибками

function updateProfile(id, changes) {
  const profile = profiles.find((p) => p.id === id);
  if (!profile) {
    throw new Error('Profile not found');
  }
  return { ...profile, ...changes };
}
 
function getDisplayName(user) {
  return user.nickname ?? user.fullName;
}
 
function toPublicProfile(profile) {
  const { passwordHash, email, ...rest } = profile;
  return rest;
}

Если скопировать этот файл в проект с включённым --strict, компилятор выдаст сразу несколько ошибок вида «Parameter implicitly has an any type». Это значит, что TypeScript не смог вывести тип параметра самостоятельно и ждёт от нас явной аннотации.

Список необходимых правок

  1. Указать типы параметров id и changes в функции updateProfile
  2. Типизировать параметр user в функции getDisplayName
  3. Учесть, что свойство nickname у пользователя может отсутствовать
  4. Создать отдельный тип для «публичного» профиля без пароля и почты и использовать его в toPublicProfile

Разбор ошибок компилятора

Прежде чем переходить к решению, полезно понять, откуда именно берётся каждая ошибка и какой приём из предыдущих уроков её закрывает. Дальше мы разберём три группы проблем: неявный any в параметрах, опциональные свойства и производные типы через Omit.

Ошибки implicit any в параметрах функций

Первым делом опишем форму профиля с помощью интерфейса — так у нас появится единый источник правды для типов во всём модуле.

interface Profile {
  id: string;
  fullName: string;
  nickname?: string;
  email: string;
  passwordHash: string;
}

Теперь можно типизировать параметры функции updateProfile. Обратите внимание, что параметр changes — это не полный профиль, а лишь набор полей, которые нужно обновить, поэтому здесь пригодится знакомая по 19 уроку утилита Partial.

function updateProfile(id: string, changes: Partial<Profile>): Profile {
  const profile = profiles.find((p) => p.id === id);
  if (!profile) {
    throw new Error('Profile not found');
  }
  return { ...profile, ...changes };
}

Тип Partial<Profile> делает все свойства Profile необязательными, поэтому в changes можно передать хоть одно поле, хоть все сразу — и TypeScript всё равно проверит, что переданные ключи и значения соответствуют исходному интерфейсу.

Опциональные свойства и union с undefined

Функция getDisplayName обращается к полю nickname, которое в интерфейсе Profile помечено вопросительным знаком. Это означает, что фактический тип этого свойства — string | undefined, и компилятор ожидает, что мы аккуратно обработаем случай отсутствия значения.

function getDisplayName(user: Profile): string {
  return user.nickname ?? user.fullName;
}

Оператор нулевого слияния ?? проверяет именно null и undefined, поэтому он идеально подходит для такого сценария: если псевдонима нет, функция вернёт полное имя пользователя, а тип результата остаётся простым и предсказуемым — string.

Если поле объекта иногда отсутствует, старайтесь описывать его через необязательный модификатор ?, а не через ручное объединение с undefined в отдельной переменной. Так TypeScript сам подскажет во всех местах использования, что перед обращением к полю стоит подстраховаться, и не даст случайно забыть проверку.

Создание производных типов с помощью Omit

Последняя ошибка касается функции toPublicProfile: она должна возвращать профиль без пароля и почты, но отдельного типа для такой «урезанной» версии пока нет. Переписывать интерфейс с нуля не нужно — гораздо удобнее получить новый тип из уже существующего Profile с помощью утилиты Omit, знакомой по уроку про Partial, Pick и Omit.

type PublicProfile = Omit<Profile, 'passwordHash' | 'email'>;
 
function toPublicProfile(profile: Profile): PublicProfile {
  const { passwordHash, email, ...rest } = profile;
  return rest;
}

Omit<Profile, 'passwordHash' | 'email'> берёт исходный интерфейс Profile и убирает из него перечисленные через union ключи, формируя новый тип со всеми оставшимися полями. Благодаря этому переменная rest после деструктуризации автоматически подходит под тип PublicProfile, и компилятору не нужно ничего домысливать.

Пошаговое решение упражнения

Если собрать все правки вместе, получится компактный файл, в котором каждая функция и переменная имеют явный или корректно выведенный тип, а компилятор больше не находит поводов для ошибок.

interface Profile {
  id: string;
  fullName: string;
  nickname?: string;
  email: string;
  passwordHash: string;
}
 
type PublicProfile = Omit<Profile, 'passwordHash' | 'email'>;
 
const profiles: Profile[] = [];
 
function updateProfile(id: string, changes: Partial<Profile>): Profile {
  const profile = profiles.find((p) => p.id === id);
  if (!profile) {
    throw new Error('Profile not found');
  }
  return { ...profile, ...changes };
}
 
function getDisplayName(user: Profile): string {
  return user.nickname ?? user.fullName;
}
 
function toPublicProfile(profile: Profile): PublicProfile {
  const { passwordHash, email, ...rest } = profile;
  return rest;
}

После этих изменений запуск tsc --noEmit завершается без единой ошибки: компилятор проверил типы, но, как и ожидалось, не создал ни одного файла на выходе. Это удобный способ убедиться, что решение действительно устраняет все проблемы, а не просто «затыкает» их приведением к any.

Возьмите за привычку запускать tsc --noEmit перед коммитом или сразу после решения похожих упражнений. Эта команда проверяет типы во всём проекте, ничего не компилируя, поэтому её удобно добавить в git-хук или в шаг CI — так ошибки не доедут до соседей по команде.

Итоги

В этом упражнении мы прошли путь от неявного any до полностью типизированного модуля: описали форму данных через интерфейс, использовали Partial для частичных обновлений, опциональные свойства для необязательных полей и Omit для создания «урезанной» версии типа.

В следующем уроке мы перейдём от отдельных упражнений к реальному проекту: разберём структуру full-stack приложения Event Me и наметим план его превращения в TypeScript-проект с типобезопасностью на бэкенде и фронтенде.

Теги: