Перейти к содержанию

Сужение типов

У функции отбора всегда был массив результата, в том числе пустой. Поиск одного курса устроен иначе: запись с нужным id может существовать, а может отсутствовать. Описать результат как безусловный Course означало бы обещать то, чего данные не гарантируют. Введём результат Course | undefined и научимся отделять эти варианты.

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

Поиск не обещает наличие

В самостоятельном снимке lesson-05 файлы data.ts и model.ts остаются прежними. Полностью замените src/app.ts:

import { courses } from './data.js';
import type { Course } from './model.js';

function findCourse(id: string): Course | undefined {
  return courses.find(course => course.id === id);
}

function describeLookup(course: Course | undefined): string {
  if (course === undefined) return 'Курс не найден';
  return `${course.title}: ${course.lessons} уроков`;
}

console.log(describeLookup(findCourse('javascript')));
console.log(describeLookup(findCourse('missing')));

Ожидаются строки:

Современный JavaScript: 20 уроков
Курс не найден

findCourse получает произвольный идентификатор и вызывает find. Имя функции не доказывает наличие записи. При javascript она вернёт объект, при missing — undefined. Аннотация результата честно описывает оба варианта. Это не новая реализация поиска, а ясная граница для вызывающего кода.

В describeLookup начальный параметр имеет тот же составной тип. Первая ветка сравнивает его именно с undefined. Если курс отсутствует, функция сразу возвращает сообщение. В оставшейся части этот вариант уже не может продолжиться, поэтому чтение course.title и course.lessons имеет достаточное основание.

Здесь важен ранний return. Условие не просто проверяет значение; оно завершает одну из веток. Если удалить возврат и только показать сообщение, после условия останется путь выполнения с отсутствующим курсом. Инструмент анализирует доступные пути, а не предполагает, что любое предшествующее сравнение навсегда делает значение безопасным.

Что знает каждая ветка

Рассмотрим отдельный вариант этой же операции. Его можно использовать вместо describeLookup, сохранив остальные строки:

function describeLookup(course: Course | undefined): string {
  if (course !== undefined) {
    return `${course.title}: ${course.lessons} уроков`;
  }
  return 'Курс не найден';
}

Теперь наличие объекта определено внутри первой ветки, а сообщение отсутствия находится после неё. Ожидаемый результат двух вызовов не меняется. Положение веток можно выбирать по читаемости, пока каждый путь даёт осмысленный результат и обращается только к доступным данным.

Сужение не записывает новый тип в объект и не меняет значение при выполнении. JavaScript выполняет обычное сравнение. Инструмент использует это сравнение как информацию о том, какие операции допустимы в соответствующей области исходника. Поэтому оно полезно только в пределах логики, которая действительно подтверждает нужное свойство.

Например, проверка course.id === 'javascript' сама требует сначала иметь объект курса. Нельзя начать с чтения поля у Course | undefined, надеясь, что последующее сравнение оправдает предыдущее чтение. Сначала устанавливаем наличие, затем проверяем конкретные свойства.

Для другого объединения, скажем string | number, проверка typeof value === 'string' отделит строковую ветку. Но typeof не узнаёт принадлежность объекта интерфейсу Course: интерфейсы стираются, а обычные объекты дают общий результат object. Когда проверяем внешний объект, необходимо проверить его поля. Это будет задачей следующего урока.

Не скрывать отсутствующий результат

Короткое выражение course?.title допускает отсутствие объекта и возвращает undefined вместо чтения поля. В некоторых местах оно подходит, например для запасной подписи course?.title ?? 'Курс не найден'. Но оно не объясняет, почему курс отсутствует, и не заменяет отдельную политику ошибки, если операция должна обязательно найти существующую запись.

Опаснее механически использовать course!. Это утверждение автора, что значение не является null или undefined; оно не делает поиск успешным и не добавляет runtime-проверку. На фиксированном javascript ошибка может долго не проявляться, а после изменения параметра появится обращение к отсутствующему объекту. В этом курсе мы выбираем явную ветку.

Аналогично findCourse(id) as Course лишь объявляет доверие к результату. Если идентификатор пришёл из URL, доверие особенно сомнительно. Правильный вопрос — какую реакцию должен увидеть пользователь при неизвестном id. Если это страница подробностей, может понадобиться настоящее состояние отсутствия. Если это подборка, возможно уместна пустая выборка.

В нашем примере функция отображения возвращает строку и не бросает исключение. Значение undefined считается обычным исходом поиска. Это позволяет вызывающему коду сознательно показывать сообщение. Программа, где неизвестный id означает нарушение внутреннего договора, могла бы иметь другую функцию, которая проверяет результат и бросает ошибку. Обе политики допустимы, если их имена и типы честны.

Сужение и изменение переменной

Когда значение может меняться, прежняя проверка не является разрешением на любое будущее использование. Особенно внимательны будьте к обработчикам и отложенным callback: между проверкой и их выполнением состояние приложения может измениться. Удобный приём — передавать уже найденный курс как параметр конкретной функции или сохранить устойчивую локальную ссылку.

При этом сохранение ссылки не замораживает поля объекта. Оно лишь фиксирует, какой объект вы используете. Если другая часть программы меняет его название, статическая проверка наличия не предотвращает такое изменение. Типовая модель и управление мутациями решают разные задачи; readonly мы введём отдельно в восьмом уроке.

Сравнения с пустой строкой также заслуживают внимания. Проверка истинности вроде if (value) может исключать не только отсутствие, но и допустимую пустую строку или ноль. Для поиска курса объект всегда истинен, однако привычка применять этот приём к любому объединению затем приводит к незаметной потере значений. Явное сравнение с undefined лучше показывает текущий смысл.

Поиск по названию не заменяет устойчивый id

У нас поиск выполняется по идентификатору, а название используется для отображения. Если переименовать «Современный JavaScript», вызов с id javascript всё ещё должен находить ту же запись. Сужение затем даст доступ к новому названию. Это разделение уменьшает связь между текстом интерфейса и механикой выбора.

Если начать искать по названию, вероятность отсутствия и неоднозначности изменится. Два курса могут получить похожую подпись, а редакционное исправление разрушит прежний поиск. Возвращаемый тип Course | undefined честно выражает отсутствие, но сам не разрешает вопрос одинаковых названий. Для такой операции может понадобиться массив результатов или отдельная проверка уникальности.

Условия также стоит соотносить с тем, что именно они доказали. Проверка найденного курса показывает наличие объекта с внутренней моделью; она не подтверждает доступность HTML-страницы по url. Отображение ссылки возможно, но успешный переход потребовал бы отдельного чтения ресурса. Статические типы не знают состояния хостинга.

Проведите мысленный сценарий без первого курса в данных. Вызов findCourse('javascript') теперь должен перейти в сообщение отсутствия, хотя остальные три записи остаются корректными. Значит, ветка действительно зависит от данных, а не от уверенности автора в постоянном составе учебного массива. После чтения оставьте исходные четыре записи, чтобы последующие сводки и ошибки схемы объяснялись на одной стабильной модели.

Связь сужения с ветками описана в Narrowing. В нашей модели два ожидаемых исхода поиска полностью различаются в тексте результата. Попробуйте заменить первый id на markdown: должна измениться только подпись найденного курса, а политика отсутствия останется прежней. Этот исходник не запускался при подготовке.

Мы ещё доверяем локальным объектам из data.ts. При чтении JSON так поступать нельзя: отсутствие записи и неверная форма записи — разные проблемы. В следующем уроке начнём с unknown, проверим внешние значения и только после этого разрешим программе обращаться к ним как к курсам.

Оглавление курса.