Сегодня нас ждет один из самых важных и практичных уроков во всем курсе. Мы будем говорить о механизме, который лежит в самой основе того, как TypeScript понимает наш код и обеспечивает его безопасность. Речь пойдет о Сужении типов (Narrowing) и Защитниках типов (Type Guards).
До этого мы часто сталкивались с ситуациями, когда у переменной могло быть несколько типов (объединение или union type). Например, string | number. И когда мы пытались выполнить операцию, специфичную для строки, TypeScript тут же жаловался: «Эй, это может быть и number, я не уверен!». Сужение типов это именно тот процесс, с помощью которого мы говорим TypeScript: «Не волнуйся, в этой конкретной точке кода переменная точно имеет такой-то тип». Давайте же узнаем, как это делается.
Как TypeScript сужает типы внутри условий
Представьте себя на месте TypeScript. Вы видите переменную value: string | number. Вы умный компилятор, но вы не можете заглянуть в будущее и узнать, что же придет в переменную во время выполнения программы. Вы должны быть уверены на 100%, что код безопасен.
Единственный способ сделать это, проанализировать поток управления программой (control flow analysis). TypeScript отслеживает все проверки, условия, присваивания и на их основе делает выводы о типе переменной в определенных блоках кода. Этот процесс и называется сужением типа.
Сужение, это переход от более общего типа (например, string | number) к более конкретному (например, string) в определенной области видимости. TypeScript проделывает это автоматически, когда встречает конструкции, которые он может проанализировать: if, switch, тернарные операторы, циклы, проверки на истинность и т.д.
Давайте рассмотрим простейший пример без всяких специальных операторов:
function printId(id: number | string) { if (typeof id === 'string') { // В этой ветке TypeScript знает, что `id` это строка. // Поэтому мы можем безопасно вызывать методы строк. console.log(id.toUpperCase()); // OK! } else { // А в этой ветке TypeScript понимает, что `id` это не строка. // Поскольку изначально тип был только string | number, // значит здесь `id` это number. console.log(id.toFixed(2)); // OK! } }
Вот как это работает: зайдя в блок if, TypeScript сузил тип id с string | number до string. Соответственно, в блоке else остался только тип number. Это и есть сужение в своей самой простой и мощной форме.
Теперь давайте изучим конкретные инструменты, которые мы можем использовать для управления этим процессом.
Использование typeof для проверки примитивных типов
Первый и самый частый инструмент в нашем арсенале это оператор typeof. Он идеально подходит для проверки примитивных типов JavaScript.
В TypeScript typeof возвращает следующие строковые значения: "string", "number", "boolean", "symbol", "undefined", "object", "function".
Важное замечание: Обратите внимание, что для null typeof возвращает "object". Это известное поведение JavaScript и оно означает, что мы не можем использовать typeof для проверки на null!
Давайте посмотрим на практическом примере, как typeof помогает нам сужать типы.
function processValue(value: string | number | boolean) { if (typeof value === 'string') { console.log(`Строка в верхнем регистре: ${value.toUpperCase()}`); } else if (typeof value === 'number') { console.log(`Число с двумя знаками после запятой: ${value.toFixed(2)}`); } else { // Здесь TypeScript понимает, что остался только boolean console.log(`Это булево значение: ${value}`); } } processValue('Hello'); // Строка в верхнем регистре: HELLO processValue(3.14159); // Число с двумя знаками после запятой: 3.14 processValue(true); // Это булево значение: true
А вот пример, который демонстрирует ограничение с null:
function getValue(x: string | null) { if (typeof x === 'object') { // ПЛОХО! TypeScript сузил тип к `string | null`, // потому что typeof null тоже 'object'! console.log(x); // x: string | null // Мы никуда не продвинулись и все еще можем получить ошибку. } }
Для проверки на null или undefined мы используем другие методы, о которых поговорим дальше. Запомните: typeof отличный инструмент для примитивов, но не для проверки на null.
Практическая задача №1
Напишите функцию calculate, которая принимает два аргумента a и b. Каждый аргумент может быть либо числом, либо строкой, содержащей число (например, '5'). Функция должна возвращать сумму a и b как число. Используйте typeof и сужение типов, чтобы преобразовать строки в числа перед сложением.
Решение:
function calculate(a: number | string, b: number | string): number { // Сужаем тип a и преобразуем его к number при необходимости const numA = typeof a === 'string' ? parseFloat(a) : a; // Сужаем тип b и преобразуем его к number при необходимости const numB = typeof b === 'string' ? parseFloat(b) : b; return numA + numB; } console.log(calculate(10, '5.5')); // 15.5 console.log(calculate('20', '30')); // 50
Использование instanceof для проверки экземпляров классов
Если typeof отлично работает с примитивами, то для проверки принадлежности объекта к определенному классу или конструктору мы используем оператор instanceof.
Оператор instanceof проверяет, присутствует ли в цепочке прототипов объекта определенный класс. Это мощный способ отличить, скажем, массив от даты или ваш собственный класс User от класса Product.
class Dog { bark() { console.log('Woof!'); } } class Cat { meow() { console.log('Meow!'); } } function makeSound(animal: Dog | Cat) { if (animal instanceof Dog) { // TypeScript сузил тип до Dog, поэтому метод bark доступен. animal.bark(); // OK! } else { // TypeScript понимает, что здесь animal - это Cat. animal.meow(); // OK! } } const myDog = new Dog(); const myCat = new Cat(); makeSound(myDog); // Woof! makeSound(myCat); // Meow!
instanceof отлично работает и со встроенными классами JavaScript, такими как Array, Date, RegExp, Map, Set и т.д.
function logValue(value: Date | RegExp | number[]) { if (value instanceof Date) { console.log(`Это Дата: ${value.toISOString()}`); } else if (value instanceof RegExp) { console.log(`Это Регулярное выражение: ${value.source}`); } else { // TypeScript сузил тип до number[] console.log(`Это Массив чисел: ${value.join(', ')}`); } } logValue(new Date()); // Это Дата: 2023-10-05T12:34:56.789Z logValue(/test/gi); // Это Регулярное выражение: test logValue([1, 2, 3]); // Это Массив чисел: 1, 2, 3
Практическая задача №2
Создайте два класса: Circle (с методом calculateArea(), возвращающим площадь по радиусу) и Rectangle (с методом calculateArea(), возвращающим площадь по ширине и высоте). Напишите функцию getArea(shape: Circle | Rectangle), которая использует instanceof для сужения типа и вызывает правильный метод для вычисления площади.
Решение:
class Circle { constructor(public radius: number) {} calculateArea(): number { return Math.PI * this.radius ** 2; } } class Rectangle { constructor(public width: number, public height: number) {} calculateArea(): number { return this.width * this.height; } } function getArea(shape: Circle | Rectangle): number { if (shape instanceof Circle) { // shape сужено до Circle return shape.calculateArea(); } else { // shape сужено до Rectangle return shape.calculateArea(); } } const circle = new Circle(5); const rectangle = new Rectangle(4, 6); console.log(getArea(circle)); // ~78.54 console.log(getArea(rectangle)); // 24
Проверки на равенство (===, !==, ==, !=) и проверка на truthy/falsy
Часто бывает нужно проверить переменную на конкретное значение. Особенно это полезно для работы с литеральными типами (literal types) и с null/undefined.
Проверка на null и undefined
Вот где проверки на равенство проявляют себя во всей красе. Помните проблему typeof null? Здесь ее можно легко решить.
function greet(name: string | null) { if (name === null) { console.log('Привет, гость!'); } else { // TypeScript точно знает, что здесь name - это string console.log(`Привет, ${name.toUpperCase()}!`); } } greet('Максим'); // Привет, МАКСИМ! greet(null); // Привет, гость!
Аналогично работает с undefined. TypeScript также понимает проверки на равенство с конкретными значениями.
type Direction = 'left' | 'right' | 'up' | 'down'; function move(direction: Direction | null) { if (direction === 'left') { console.log('Двигаемся влево'); } else if (direction === 'right') { console.log('Двигаемся вправо'); } else if (direction === null) { console.log('Остаемся на месте'); } else { // Сужено до 'up' | 'down' console.log('Двигаемся по вертикали'); } }
Проверки на truthy/falsy
Иногда нам не важно, какое именно значение, а важно, есть ли оно вообще. Мы можем использовать саму переменную в условии, чтобы проверить, является ли она truthy (не false, 0, "", null, undefined, NaN).
function printAll(strs: string | string[] | null) { // Проверяем, не является ли strs falsy значением (включая null и пустую строку) if (strs && typeof strs === 'object') { // strs сужено до string[] (но не пустой массив!) for (const s of strs) { console.log(s); } } else if (typeof strs === 'string') { // strs сужено до string console.log(strs); } else { // strs сужено до null или другого falsy console.log('Нет данных'); } } printAll(['A', 'B', 'C']); // A, B, C printAll('Hello'); // Hello printAll(''); // Пустая строка (falsy), но тип string! Попадет во вторую ветку. printAll(null); // Нет данных
Важно: Будьте осторожны с 0 и пустой строкой "". Они являются falsy, но это валидные значения, которые иногда нужно обрабатывать отдельно.
Практическая задача №3
Напишите функцию getLength, которая принимает аргумент типа string | number | null | undefined. Функция должна возвращать длину строки или количество цифр в числе. Если аргумент null или undefined, функция должна возвращать 0. Используйте проверки на равенство для обработки null и undefined.
Решение:
function getLength(input: string | number | null | undefined): number { if (input === null || input === undefined) { return 0; } // После проверки на null/undefined, input сужен до string | number if (typeof input === 'string') { return input.length; } else { // input сужен до number return input.toString().length; } } console.log(getLength('Hello')); // 5 console.log(getLength(12345)); // 5 console.log(getLength(null)); // 0 console.log(getLength(undefined)); // 0
Пользовательские type guards (Защитники типов)
А что, если встроенных проверок недостаточно? Например, мы хотим проверить, что объект имеет определенную структуру (проверить наличие свойства). Или у нас сложная логика валидации. Для этого TypeScript позволяет нам создавать собственные Защитники типов (User-Defined Type Guards).
Пользовательский защитник типа это функция, которая возвращает булево значение и имеет специальную сигнатуру возвращаемого типа: arg is Type.
Эта сигнатура is и есть волшебство. Она говорит TypeScript: «Если эта функция вернет true, то переданный аргумент имеет указанный тип Type«.
Проверка наличия свойства
Самый частый случай, это проверка, что у объекта есть определенное свойство (проверка на существование).
// Допустим, у нас есть два типа type Fish = { swim: () => void }; type Bird = { fly: () => void }; // Наш пользовательский type guard function isFish(pet: Fish | Bird): pet is Fish { // Мы проверяем, есть ли у объекта метод swim. // Приведение (pet as Fish) нужно, чтобы TypeScript не ругался на возможное отсутствие свойства. return (pet as Fish).swim !== undefined; } function howToMove(pet: Fish | Bird) { if (isFish(pet)) { // TypeScript доверяет нашей функции isFish и сужает тип до Fish. pet.swim(); // OK! } else { // Следовательно, здесь тип сужен до Bird. pet.fly(); // OK! } } const myFish: Fish = { swim: () => console.log('Плыву!') }; const myBird: Bird = { fly: () => console.log('Лечу!') }; howToMove(myFish); // Плыву! howToMove(myBird); // Лечу!
Более сложная валидация
Защитники типов могут содержать любую логику. Например, проверку на число в строке.
function isNumberString(value: any): value is string { // Проверяем, что value - это строка И что она преобразуется в корректное число return typeof value === 'string' && !isNaN(parseFloat(value)); } function processInput(input: string | number) { if (isNumberString(input)) { // TypeScript знает, что input - string, но наша guard-функция // дала ему больше уверенности. Хотя на самом деле тип все еще string. // Чаще это используется для более сложных структур. const num = parseFloat(input); console.log(`Число из строки: ${num}`); } else if (typeof input === 'number') { console.log(`Изначальное число: ${input}`); } } processInput('123'); // Число из строки: 123 processInput(456); // Изначальное число: 456 processInput('abc'); // Не пройдет проверку isNumberString, но и не number. Ничего не выведет.
Практическая задача №4
Создайте тип Car (свойство type: 'car' и метод drive()) и тип Boat (свойство type: 'boat' и метод sail()). Напишите пользовательский защитник типа isCar, который проверяет, является ли объект машинкой. Затем напишите функцию useVehicle, которая использует этот защитник для вызова правильного метода.
Решение:
interface Car { type: 'car'; drive: () => void; } interface Boat { type: 'boat'; sail: () => void; } // Пользовательский type guard function isCar(vehicle: Car | Boat): vehicle is Car { // Проверяем наличие свойства и его значение return vehicle.type === 'car'; } function useVehicle(vehicle: Car | Boat) { if (isCar(vehicle)) { vehicle.drive(); } else { vehicle.sail(); } } const myCar: Car = { type: 'car', drive: () => console.log('Машина едет по дороге') }; const myBoat: Boat = { type: 'boat', sail: () => console.log('Лодка плывет по воде') }; useVehicle(myCar); // Машина едет по дороге useVehicle(myBoat); // Лодка плывет по воде
Комбинирование подходов и заключение
В реальном коде вы будете часто комбинировать все эти подходы для создания надежной и понятной системы типов.
function processInput(input: unknown) { // 1. Сначала проверим, не null ли и не undefined ли это if (input == null) { console.log('Нет данных'); return; } // 2. input теперь сужен от unknown к чему-то, что не null/undefined if (typeof input === 'string') { console.log(`Строка: ${input.trim()}`); } else if (typeof input === 'number') { console.log(`Число: ${input.toFixed(2)}`); } else if (Array.isArray(input)) { // Array.isArray - это встроенный type guard! console.log(`Массив: ${input.length} элементов`); } else if (isCustomType(input)) { // Наш пользовательский guard console.log(`Кастомный тип: ${input.customValue}`); } // ... и так далее }
Используйте typeof и instanceof для простых случаев, проверки на равенство для null и литеральных типов и создавайте собственные защитники типов для сложной валидации объектов.
Удачи в изучении и до встречи на следующих уроках, где мы углубимся в работу с Общими типами (Generics)!
Это был урок №20 из моего полного курса по TypeScript для начинающих. Хотите освоить язык от основ до продвинутых тем? Переходите по ссылке и начните обучение прямо сейчас!
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


