Avatar ·

Что делает ключевое слово `is` в TypeScript и как его правильно использовать?

Всем привет! Я начал изучать TypeScript и наткнулся на ключевое слово `is`, которое используется в некоторых примерах. Честно говоря, я не совсем понимаю, зачем оно нужно и чем отличается от обычного возвращаемого типа `boolean`. Вот пример, который я нашел: ```typescript function isString(value: unknown): boolean { return typeof value === "string"; } ``` Но я видел и такой вариант: ```typescript function isString(value: unknown): value is string { return typeof value === "string"; } ``` В чем разница? Я понимаю, что `value is string` в возвращаемом типе — это какая-то специальная конструкция, но не могу уловить, когда её использовать. Если я просто возвращаю `boolean`, TypeScript ведь тоже может понять, что значение — строка, после проверки? Или нет? Попробовал написать такой код: ```typescript function isString(value: unknown): boolean { return typeof value === "string"; } function processValue(value: unknown) { if (isString(value)) { console.log(value.toUpperCase()); // Ошибка: Object is of type 'unknown' } } ``` TypeScript ругается на `value.toUpperCase()`. Почему? Ведь я же проверил, что это строка! Буду благодарен за подробное объяснение. Может быть, есть какие-то продвинутые сценарии использования `is`, о которых стоит знать новичку? <<>>

Что такое type guard (защитник типа) и как работает `is`

Ключевое слово is в TypeScript — это часть механизма type guard (защитника типа). Оно создает пользовательскую функцию, которая не просто возвращает true/false, но и сужает тип аргумента для TypeScript внутри условных блоков.

Ваша проблема в примере с boolean именно в этом. Когда вы пишете:

function isString(value: unknown): boolean {
  return typeof value === "string";
}

TypeScript видит только: "Функция возвращает boolean". Он не знает, что именно вы проверяли. Для него true — это просто число true, а не "значение теперь точно строка". Поэтому после вызова isString(value) тип value остается unknown.

Как работает `value is string`

Конструкция value is string в возвращаемом типе говорит TypeScript:

  • "Если эта функция возвращает true, то аргумент value имеет тип string"
  • "Если возвращает false — тип не меняется"

Исправленный пример:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function processValue(value: unknown) {
  if (isString(value)) {
    // Теперь TypeScript знает: value — это string
    console.log(value.toUpperCase()); // ✅ Работает!
  } else {
    // Здесь value все еще unknown
    console.log("Not a string");
  }
}

Продвинутые сценарии использования

1. Проверка на интерфейсы и сложные типы
Часто используется для проверки, соответствует ли объект определенному интерфейсу:

interface Cat {
  meow(): void;
  name: string;
}

interface Dog {
  bark(): void;
  name: string;
}

function isCat(pet: Cat | Dog): pet is Cat {
  return (pet as Cat).meow !== undefined;
}

function makeSound(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow(); // ✅ TypeScript знает: это Cat
  } else {
    pet.bark(); // ✅ TypeScript знает: это Dog
  }
}

2. Работа с discriminated unions (размеченные объединения)
Даже если у вас есть общее поле (например type), is может пригодиться для сложной логики:

type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function isCircle(shape: Shape): shape is { kind: "circle"; radius: number } {
  return shape.kind === "circle";
}

function getArea(shape: Shape): number {
  if (isCircle(shape)) {
    return Math.PI * shape.radius ** 2; // ✅ radius доступен
  }
  return shape.side ** 2; // ✅ side доступен
}

3. Работа с массивами и фильтрацией
is отлично работает с Array.filter():

function isNumber(value: unknown): value is number {
  return typeof value === "number";
}

const mixedArray: unknown[] = [1, "hello", 2, "world", 3];
const numbers: number[] = mixedArray.filter(isNumber);
// ✅ TypeScript понимает, что numbers — это number[]

Ограничения

  • Вы обязаны написать корректную логику проверки внутри функции. TypeScript не проверяет, правильно ли вы реализовали условие.
  • Нельзя использовать is для асинхронных функций — type guard должен быть синхронным.
  • Работает только с одним параметром (первым аргументом функции).

В итоге: is — это мощный инструмент, который помогает TypeScript "понимать" вашу логику и автоматически сужать типы, избавляя вас от лишних приведений типов (as).

<<>>

Альтернативный подход: встроенные защитники типов

Прежде чем писать собственные функции с is, стоит знать, что TypeScript уже имеет встроенные защитники типов, которые работают "из коробки":

  • typeof — для примитивов: typeof x === "string"
  • instanceof — для классов: x instanceof Date
  • in — для проверки свойств: "property" in obj
  • Array.isArray() — проверка на массив

Пример со встроенным защитником:

function process(value: unknown) {
  if (typeof value === "string") {
    // ✅ TypeScript автоматически сужает тип до string
    console.log(value.toUpperCase());
  }
  
  if (value instanceof Date) {
    // ✅ TypeScript знает, что это Date
    console.log(value.getFullYear());
  }
}

Когда использовать собственный is?
Только когда вам нужно объединить несколько проверок в одну функцию или проверить сложную структуру данных (например, интерфейс). Для простых случаев встроенные механизмы TypeScript вполне достаточны и даже предпочтительнее, так как они не требуют написания дополнительного кода и не могут содержать ошибок в логике проверки.

Также помните, что is — это не магия. Если вы напишете:

function isString(val: unknown): val is string {
  return true; // ❌ Всегда возвращает true
}

TypeScript поверит вам, и это может привести к ошибкам в рантайме. Всегда проверяйте корректность своей логики.