Что такое Type Guard в TypeScript?

Ответ

Type Guard (защита типа) в TypeScript — это выражение, которое выполняет проверку во время выполнения (runtime) и позволяет компилятору сузить тип переменной внутри определенной области видимости (например, внутри блока if). Это ключевой механизм для безопасной работы с union-типами, unknown или any.

Основные виды Type Guards:

  1. typeof Guard: Для примитивных типов.

    function processValue(value: string | number) {
      if (typeof value === 'string') {
        // Здесь TypeScript знает, что value — строка
        console.log(value.toUpperCase());
      } else {
        // Здесь value — число
        console.log(value.toFixed(2));
      }
    }
  2. instanceof Guard: Для проверки принадлежности к классу.

    class ApiError extends Error {
      statusCode: number;
      constructor(message: string, statusCode: number) {
        super(message);
        this.statusCode = statusCode;
      }
    }
    
    async function handleError(error: Error | ApiError) {
      if (error instanceof ApiError) {
        // Теперь мы знаем, что это ApiError
        console.error(`API Error ${error.statusCode}: ${error.message}`);
        // Можно безопасно обратиться к error.statusCode
      } else {
        console.error(`Generic Error: ${error.message}`);
      }
    }
  3. Пользовательский Type Guard (User-Defined Type Guard): Функция, возвращающая предикат типа (parameterName is Type).

    interface Cat {
      meow(): void;
    }
    interface Dog {
      bark(): void;
    }
    
    // Функция-защитник. Она говорит TypeScript: "Если я верну true, то pet — это Cat"
    function isCat(pet: Cat | Dog): pet is Cat {
      return (pet as Cat).meow !== undefined;
      // Или: return 'meow' in pet;
    }
    
    function playWithPet(pet: Cat | Dog) {
      if (isCat(pet)) {
        pet.meow(); // OK
        // pet.bark(); // Ошибка: Свойство 'bark' не существует у типа 'Cat'
      } else {
        pet.bark(); // TypeScript понимает, что здесь pet — Dog
      }
    }

Зачем это нужно в Node.js-разработке? Type Guards делают код типобезопасным при работе с внешними данными (парсинг JSON, валидация входящих HTTP-запросов), обработке ошибок разных типов или при использовании библиотек, возвращающих unknown.

Ответ 18+ 🔞

Слушай, а вот эта штука — Type Guard — это вообще пиздец как удобно, когда ты в TypeScript'е работаешь. Представь: у тебя переменная, а она может быть чем угодно — строкой, числом, каким-то твоим интерфейсом. И компилятор, блядь, на тебя смотрит как на идиота и говорит: «Ну я-то не знаю, что у тебя там сейчас, так что не дам вызвать .toUpperCase(), потому что вдруг это число?». И ты сидишь и думаешь: «Ёпта, я же вижу, что это строка!».

Вот тут-то и вырубают type guards. Это такие проверки в рантайме, которые компилятору на ушко шепчут: «Расслабься, братан, тут всё типобезопасно, я проверил». И компилятор успокаивается и сужает тип в этой конкретной области.

Какие они бывают, эти защитники?

  1. typeof Guard: Самый простой, для примитивов. Как будто спрашиваешь у переменной паспорт.

    function processValue(value: string | number) {
      if (typeof value === 'string') {
        // А вот тут TypeScript уже охуел от твоей проницательности и разрешает всё
        console.log(value.toUpperCase()); // Всё ок, value — строка
      } else {
        // А сюда уже попадёт только число, так что можно его округлять
        console.log(value.toFixed(2));
      }
    }
  2. instanceof Guard: Когда нужно понять, от какого класса твой объект. Типа «ты чей, мальчик?».

    class ApiError extends Error {
      statusCode: number;
      constructor(message: string, statusCode: number) {
        super(message);
        this.statusCode = statusCode;
      }
    }
    
    async function handleError(error: Error | ApiError) {
      if (error instanceof ApiError) {
        // Всё, попался, пидарас шерстяной! Теперь ясно, что это наша кастомная ошибка
        console.error(`API Error ${error.statusCode}: ${error.message}`);
        // И можно спокойно лезть в error.statusCode, доверия — ебать ноль, но типобезопасно!
      } else {
        console.error(`Generic Error: ${error.message}`);
      }
    }
  3. Пользовательский Type Guard: Вот это уже высший пилотаж, когда стандартных проверок не хватает. Ты пишешь свою функцию, которая возвращает не просто boolean, а предикат типа (parameterName is Type). Это как дать компилятору официальную справку с печатью.

    interface Cat {
      meow(): void;
    }
    interface Dog {
      bark(): void;
    }
    
    // Смотри, магия: функция говорит — «Если я верну true, то pet — это 100% Cat, ёпта».
    function isCat(pet: Cat | Dog): pet is Cat {
      return (pet as Cat).meow !== undefined;
      // Или можно так: return 'meow' in pet;
    }
    
    function playWithPet(pet: Cat | Dog) {
      if (isCat(pet)) {
        pet.meow(); // Ок, это кот, можно мурякать
        // pet.bark(); // Ошибка компиляции! У кота такого метода нет, хитрая жопа!
      } else {
        pet.bark(); // TypeScript сам догадался, что если не кот — то собака
      }
    }

А нахуя это всё в Node.js? Да везде, блядь! Парсишь JSON от какого-нибудь кривого API — там unknown или any. Валидируешь тело HTTP-запроса — опять union-типы. Обрабатываешь ошибки, которые могут быть и системными, и твоими кастомными. Без type guards ты будешь постоянно делать as SomeType, а это прямой путь в ад, где в рантайме всё падает с Cannot read property 'meow' of undefined. Так что это не прихоть, а вопрос выживания в диком мире асинхронности и внешних данных.