Type Guards. Фундамент типобезопасной разработки

Frontend

Сегодня без долгих вступлений. Если вы пишете на TypeScript, для сохранения ментального здоровья вам просто необходимо уметь пользоваться тайп гардами — инструментом, который позволяет вам указать TypeScript, с каким именно типом он работает.

Зачем нужны Type Guards?

Без Type Guards TypeScript часто не может определить тип переменной в рантайме:

function processValue(value: string | number) {
    // Ошибка! TS не знает, есть ли у value метод toUpperCase()
    return value.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on type 'string | number'
}

TypeScript буквально говорит: «Я не уверен, что это — строка или число. А что если число? У числа нет метода toUpperCase()».

Чтобы этот код сработал, мы должны как-то сузить тип и сказать TS: «Если это строка, то делай так. Если число — иначе»

Базовые Type Guards

1. typeof — для примитивов

typeof работает с примитивными типами: stringnumberbooleansymbolundefinedfunctionbigint.

function processValue(value: string | number) {
    if (typeof value === 'string') {
        // TS знает: здесь value — точно string
        return value.toUpperCase();
    } else {
        // TS знает: здесь value — точно number
        return value.toFixed(2);
    }
}
processValue('hello'); // 'HELLO'
processValue(42);      // '42.00'

TypeScript видит проверку typeof value === 'string' и внутри блока if сужает тип до string. В блоке else — до number.

2. instanceof — для классов

instanceof проверяет, является ли объект экземпляром класса.

class Dog {
    bark() { console.log('Гав!'); }
}
class Cat {
    meow() { console.log('Mяв!'); }
}
function makeSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        // TS знает: здесь animal — Dog
        animal.bark();
    } else {
        // TS знает: здесь animal — Cat
        animal.meow();
    }
}
makeSound(new Dog()); // 'Гав!'
makeSound(new Cat()); // 'Мяв!'

3. in — для проверки свойств

in проверяет, есть ли у объекта определенное свойство.

interface Car {
    drive(): void;
}
interface Boat {
    sail(): void;
}
function move(vehicle: Car | Boat) {
    if ('drive' in vehicle) {
        // TS знает: здесь vehicle — Car (у него есть drive)
        vehicle.drive();
    } else {
        // TS знает: здесь vehicle — Boat (у него нет drive, значит есть sail)
        vehicle.sail();
    }
}

4. Строгие проверки на null и undefined

function processName(name: string | null) {
    if (name === null) {
        return 'Имя не указано';
    }
    // TS знает: здесь name — точно string
    return name.toUpperCase();
}

Пользовательские Type Guards

Иногда (и даже чаще всего) встроенных проверок недостаточно. Например, нужно проверить, что объект соответствует сложному интерфейсу.

interface User {
    name: string;
    age: number;
}
// Мы получили данные из API - это any или unknown
const data: unknown = JSON.parse('{"name":"Vasya","age":30}');
// Проверяем, что data - это User
function isUser(obj: unknown): obj is User {
    // Проверяем, что obj - объект и у него есть поля name (string) и age (number)
    return (
        typeof obj === 'object' &&
        obj !== null &&
        'name' in obj &&
        typeof obj.name === 'string' &&
        'age' in obj &&
        typeof obj.age === 'number'
    );
}
if (isUser(data)) {
    // TS знает: здесь data — User!
    console.log(data.name.toUpperCase()); // 'VASYA'
}

Обратите внимание на тип возвращаемого нашим тайпгардом значения. obj is User — это предикат типа. Он говорит TypeScript: «Если эта функция возвращает true, то переменная obj имеет тип User«.

Дополнительные техники

1. Дискриминируемые юнионы (Discriminated Unions)

Про дискриминируемые юнионы мы уже говорили, но тут напомню вкратце. Это когда у всех типов в объединении есть общее свойство-дискриминант, по которому легко определить тип.

interface Circle {
    kind: 'circle';
    radius: number;
}
interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}
interface Triangle {
    kind: 'triangle';
    base: number;
    height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            // TS знает: здесь shape — Circle
            return Math.PI * shape.radius ** 2;
        case 'rectangle':
            // TS знает: здесь shape — Rectangle
            return shape.width * shape.height;
        case 'triangle':
            // TS знает: здесь shape — Triangle
            return (shape.base * shape.height) / 2;
    }
}

TypeScript автоматически сужает тип на основе значения kind. И если вы добавите новый тип в Shape, но не добавите его в switch, TypeScript выплюнет ошибку (но это если включить exhaustive checking в tsconfig.json).

2. Проверка массива

function isStringArray(value: unknown): value is string[] {
    return Array.isArray(value) && value.every(item => typeof item === 'string');
}
const data: unknown = ['a', 'b', 'c'];
if (isStringArray(data)) {
    // TS знает: data — string[]
    console.log(data.join(', ')); // 'a, b, c'
}

3. Проверка объединения с пользовательским предикатом

type ApiResponse = 
    | { status: 'success'; data: User }
    | { status: 'error'; error: string };

function isSuccess(response: ApiResponse): response is { status: 'success'; data: User } {
    return response.status === 'success';
}

function handleResponse(response: ApiResponse) {
    if (isSuccess(response)) {
        // TS знает: здесь response — success
        console.log(response.data.name);
    } else {
        // TS знает: здесь response — error
        console.log('Ошибка:', response.error);
    }
}

Для чего вам могут пригодиться Type Guards?

  1. Валидация API-ответов — после fetch() вы получаете unknown. Проверяете через пользовательский Type Guard.
  2. Работа с формами — проверка, что значение — строка, число, email и т.д.
  3. Redux Toolkit — в редьюсерах проверяете, какой экшен пришел.
  4. React-компоненты с пропсами разных типов — внутри компонента проверяете, что именно передано.
  5. Обработка ошибок — проверка, что ошибка — это экземпляр Error, а не строка.

Итого

  • Type Guard — способ сузить широкий тип (string | number) до конкретного (string)
  • Встроенные: typeof (примитивы), instanceof (классы), in (свойства).
  • Пользовательский Type Guard: функция, которая возвращает value is Type.
  • Дискриминируемые юнионы — когда у всех типов есть общее поле, по которому можно определить тип.
  • Используйте Type Guards для валидации API-ответов, проверки форм и обработки ошибок.

Используйте тайп гарды, чтобы избавиться от any и сделать ваш код типобезопасным. Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу