ПрограммированиеFrontend разработчик

Как TypeScript работает с user-defined type guards (самописными функциями-защитниками типов), для чего они нужны, и как создавать корректные type predicate-функции? Какие типичные ловушки встречаются при их реализации?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса: TypeScript расширяет обычные проверки типов, позволив разработчикам создавать свои пользовательские функции – type guards – которые проверяют, соответствует ли объект определенному типу. Это необходимо для работы с union-типами, динамическими структурами и API, где тип значения может быть разным.

Проблема часто в том, что обычные проверки типа через typeof и instanceof ограничены примитивами и классами, а вот структуры или сложные типы без этого не определишь. Нужно уметь явно подсказывать компилятору в каких случаях значение безопасно сужается до нужного типа.

Решение — писать функции-защитники вида function isCat(obj: Animal): obj is Cat {...}, где ключевой момент — type predicate в возвращаемом типе функции.

Пример кода:

interface Dog { bark: () => void; } interface Cat { meow: () => void; } type Pet = Dog | Cat; function isDog(pet: Pet): pet is Dog { return (pet as Dog).bark !== undefined; } function makeSound(pet: Pet) { if (isDog(pet)) { pet.bark(); } else { pet.meow(); } }

Ключевые особенности:

  • Проверка типа реализуется через специальный type predicate-функции вида param is Type.
  • Можно создавать свои guards и использовать их для любых структур, не только для объектов-классов.
  • Некорректные type guards могут привести к ошибкам времени выполнения и неверному сужению типа.

Вопросы с подвохом.

Достаточно ли в type guard-функции вернуть true/false, чтобы компилятор сузил тип?

Нет. Важно явно указать возвращаемый тип в виде type predicate (например, pet is Dog), иначе TypeScript не будет сужать тип значения автоматически, даже если функция возвращает только true или false.

Можно ли использовать type guard внутри callback (например, в filter), и будет ли сужение работать корректно?

Да, если type guard правильно аннотирован, то компилятор сузит тип элементов массива после filter и внутри forEach/callback функций. Но если аннотация отсутствует или написана неверно, результат будет иметь тип-объединение (union) вместо конкретизированного типа.

const pets: Pet[] = [...]; const dogs = pets.filter(isDog); // TypeScript знает, что dogs: Dog[]

Чем пользовательские type guards отличаются от обычных проверок типа через typeof, instanceof?

Type guard-функции позволяют реализовать проверку любой структуры, описывать проверки любого уровня сложности, оперировать интерфейсами без класса, а не только базовыми типами и классами.

Типовые ошибки и анти-паттерны

  • Type guard не возвращает type-predicate тип, а просто boolean — нет эффекта для компилятора.
  • Проверяют только поверхностные ключи, не гарантируя структурную целостность.
  • Type guard всегда возвращает true, фактически отключая типовую защиту.
  • Входящий параметр слишком общий, либо делается неявное приведение типа без валидации.

Пример из жизни

Негативный кейс

Функция фильтрует пользователей, делая type guard без type predicate:

function isValidAdmin(user: any): boolean { return user.isAdmin === true; } const admins = users.filter(isValidAdmin); // admins: any[]

Плюсы:

  • Быстро, эффект заметен в рантайме

Минусы:

  • После фильтрации тип данных не уточняется для TypeScript, возможны ошибки при обращении к свойствам

Позитивный кейс

Функция фильтрует с корректным type predicate:

interface Admin { name: string; isAdmin: true; } function isAdmin(user: any): user is Admin { return user && user.isAdmin === true; } const admins = users.filter(isAdmin); // admins: Admin[]

Плюсы:

  • Тип результатов точно известен
  • Уменьшается вероятность ошибок при дальнейшей работе с результатом

Минусы:

  • Требует чуть большего внимания к сигнатуре и к покрытию проверками