Мы с вами уже проделали огромный путь, изучили базовые типы, функции, интерфейсы и дженерики. Мы стали увереннее в системе типов и компилятор стал нашим верным союзником, который подсказывает нам ошибки и помогает писать более надежный код.
Но сегодня мы поговорим о ситуации, когда мы становимся главнее компилятора. Ситуации, когда мы говорим ему: «Спасибо, друг, я понимаю твои опасения, но в данном конкретном случае я знаю точно, что здесь за тип. И я беру на себя ответственность». Этот инструмент называется Приведение типов (Type Assertion).
Что такое приведение типов?
Представьте себе такую ситуацию. Вы работаете с DOM (Document Object Model), структурой HTML-страницы. Вы получаете элемент по его идентификатору с помощью document.getElementById('myInput'). TypeScript, будучи хорошим статическим анализатором, не может знать наверняка, что именно вернет этот метод. Ведь на странице может не быть элемента с таким id. Поэтому он возвращает тип HTMLElement | null. Это самый общий тип для HTML-элементов и он может быть null.
Но вы-то как разработчик знаете, что на вашей странице точно есть элемент с id="myInput" и что это именно элемент <input type="text">. Вы хотите получить доступ к его специфическому свойству, например, value. Но если вы попробуете написать element.value, TypeScript выдаст ошибку: «Свойство ‘value’ не существует у типа ‘HTMLElement’». И он будет прав! У общего HTMLElement действительно нет свойства value; оно есть у HTMLInputElement.
Вот здесь на сцену и выходит приведение типов. Это механизм, который позволяет вам явно указать компилятору, какого типа является значение в данном месте кода. Вы не изменяете само значение во время выполнения (как это бывает в языках с настоящим приведением типов), вы лишь утверждаете (assert) его тип для TypeScript на этапе компиляции. Это своего рода подсказка компилятору: «Рассматривай эту переменную не как HTMLElement, а как HTMLInputElement».
Важно понимать: приведение типов не выполняет никаких специальных проверок или преобразований данных. Вы говорите компилятору «доверься мне». И если вы окажетесь неправы, это приведет к ошибке во время выполнения. Поэтому использовать этот инструмент нужно с умом и только тогда, когда вы абсолютно уверены в типе значения.
Синтаксис приведения типов: as и angle-brackets
TypeScript предоставляет два способа синтаксически записать приведение типов. Они абсолютно идентичны по своей функциональности и выбор между ними это дело вкуса и соглашений по код-стайлу.
1. Синтаксис as (предпочтительный)
Это самый распространенный и рекомендуемый синтаксис, особенно при работе с JSX (для React), где второй синтаксис может конфликтовать с разметкой.
let someValue: any = "это строка"; let strLength: number = (someValue as string).length;
В этом примере у нас есть переменная someValue типа any. Мы хотим получить ее длину, используя свойство length. Но поскольку TypeScript видит ее как any, он не гарантирует, что у нее есть это свойство. Мы же утверждаем, что someValue это строка, используя as string. После этого мы можем спокойно обращаться к .length.
Вернемся к нашему примеру с DOM:
// Без приведения типов const inputElement = document.getElementById('myInput'); console.log(inputElement.value); // Ошибка: Property 'value' does not exist on type 'HTMLElement'. // С приведением типов const inputElement = document.getElementById('myInput') as HTMLInputElement; console.log(inputElement.value); // Теперь一切正常 (теперь всё хорошо)!
2. Синтаксис угловых скобок (<type>)
Это более старый синтаксис, который пришел из языков вроде C# и Java. Он работает точно так же.
let someValue: any = "это строка"; let strLength: number = (<string>someValue).length; // Пример с DOM const inputElement = <HTMLInputElement>document.getElementById('myInput'); console.log(inputElement.value);
Какой синтаксис выбрать?
Лично я, Максим, почти всегда использую синтаксис as. Он более четко отделяется от окружающего кода и не создает неоднозначностей в файлах .tsx (где используется JSX), так как угловые скобки <div> являются частью синтаксиса разметки. Для consistency (единообразия) во всем проекте я советую использовать as.
Когда использовать приведение типов?
Давайте рассмотрим наиболее частые и правильные сценарии использования этого инструмента.
1. Работа с DOM
Это, пожалуй, самый классический и оправданный пример. TypeScript не может знать о структуре вашей HTML-страницы, поэтому он возвращает общие типы.
// Приведение к HTMLInputElement const searchInput = document.getElementById('search') as HTMLInputElement; const searchTerm = searchInput.value; // Приведение к HTMLButtonElement const submitButton = document.getElementById('submit') as HTMLButtonElement; submitButton.disabled = true; // Приведение к HTMLElement (если мы уверены, что элемент есть, но это не специфический тип) const container = document.getElementById('container') as HTMLElement; container.innerHTML = '<p>Новый контент</p>'; // Важно! Всегда проверяйте на null, если элемент может отсутствовать! const maybeElement = document.getElementById('something-that-might-not-exist'); if (maybeElement) { // TypeScript здесь уже сузит тип до HTMLElement благодаря проверке const element = maybeElement as HTMLElement; // Хотя often это уже не нужно element.classList.add('active'); }
2. Обработка данных с известной структурой (например, из JSON API)
Часто при работе с внешним API вы получаете данные в формате JSON через метод fetch. TypeScript видит результат парсинга JSON как any. Вы же, зная контракт API, можете утвердить его тип.
interface User { id: number; name: string; email: string; } fetch('https://api.example.com/user/1') .then(response => response.json()) .then((data: any) => { // Мы знаем, что API возвращает объект пользователя const user = data as User; console.log(user.name); // Теперь можно обращаться к свойствам User });
Внимание! Это опасное место. Если API внезапно изменит формат ответа и вернет что-то другое, ваше утверждение типа не спасет от ошибки выполнения. В идеале нужно делать валидацию runtime-данных (например, с помощью библиотек Zod или io-ts), но приведение типов быстрый способ для прототипов или когда вы полностью доверяете источнику данных.
3. Приведение к более конкретному или менее конкретному типу внутри union type
Иногда при работе с union-типами TypeScript может не сужать тип так, как нам хочется, особенно в сложных цепочках операций.
type Status = 'success' | 'error'; interface ApiResponse { status: Status; data?: string; error?: string; } function handleResponse(response: ApiResponse) { if (response.status === 'success') { // TypeScript знает, что здесь должен быть data, но он все еще видит тип response.data как string | undefined // Мы можем утвердить, что data точно есть const successfulData = response.data as string; // Опасно! Лучше сделать проверку console.log(successfulData.toUpperCase()); } // Более безопасный способ - делать проверку if (response.status === 'success' && response.data) { // Здесь TypeScript уже сам сузил тип response.data до string console.log(response.data.toUpperCase()); } }
Более полезный пример, приведение к менее конкретному типу в union. Например, когда функция требует широкий тип.
type Admin = { role: 'admin', permissions: string[] }; type User = { role: 'user', name: string }; type AppUser = Admin | User; function logUser(user: AppUser) { // Допустим, нам нужно передать этого пользователя в функцию, которая принимает любой объект с role // Мы можем утвердить тип до { role: string }, хотя обычно это излишне const anyRoleUser = user as { role: string }; sendToLogger(anyRoleUser); } function sendToLogger(obj: { role: string }) { }
Что такое утверждение const (const assertion)?
Это особенная и очень полезная форма приведения типов, появившаяся в TypeScript 3.4. С помощью as const мы говорим компилятору:
-
Сделай это значение read-only (только для чтения).
-
Сужай литеральные типы их до их конкретных значений, а не до общего типа (например,
'hello'вместоstring). -
Для объектов и массивов сделай их свойства и элементы read-only.
Это лучше всего понять на примере.
// Без as const let name = 'Максим'; // тип: string let arr = [1, 2, 3]; // тип: number[] // С as const let nameConst = 'Максим' as const; // тип: "Максим" let arrConst = [1, 2, 3] as const; // тип: readonly [1, 2, 3] // Пример с объектом const user = { name: 'Anna', age: 30, } as const; // Теперь user имеет тип: { readonly name: "Anna"; readonly age: 30; } // Все поля зафиксированы и не могут быть изменены. // user.name = 'Another'; // Ошибка! // Отлично подходит для создания неизменяемых конфигов const APP_CONFIG = { apiUrl: 'https://api.myapp.com', retryCount: 3, mode: 'production', } as const; // Идеально работает с union типами и функциями, которые требуют литеральные типы function setStatus(status: 'idle' | 'loading' | 'success') {} // Без as const массив будет string[], что не подходит const statuses = ['idle', 'loading', 'success']; // string[] setStatus(statuses[0]); // Ошибка: Argument of type 'string' is not assignable to parameter of type... // С as const const statusesConst = ['idle', 'loading', 'success'] as const; // readonly ["idle", "loading", "success"] setStatus(statusesConst[0]); // OK, потому что statusesConst[0] имеет тип "idle"
Утверждение as const это инструмент для создания неизменяемых структур данных с максимально узкими и точными типами, что помогает выявить больше ошибок на этапе компиляции.
Чего НЕ делать: распространенные ошибки и антипаттерны
Приведение типов это обход системы типов и его небрежное использование может свести на нет все преимущества TypeScript.
1. Использование as any как молотка для всех проблем
Это худший грех. Если вы сталкиваетесь с ошибкой типа, первым вашим побуждением не должно быть «замьютить» ее с помощью as any. Это уничтожает всю безопасность типов.
// ПЛОХО! ОЧЕНЬ ПЛОХО! const data = getSomeData() as any; doWhateverYouWant(data); // Типовая безопасность полностью отключена
Вместо этого постарайтесь понять, в чем причина ошибки и правильно опишите интерфейсы или используйте дженерики.
2. Приведение к несовместимому типу
TypeScript не позволит вам делать совершенно безумные приведения, например, преобразовывать число в объект функции, если между этими типами нет никакой совместимости.
let x = 123; let y = x as string; // Ошибка: Conversion of type 'number' to type 'string' may be a mistake...
Однако он разрешает приведение через any или unknown. Это двойная ловушка.
let x = 123; let y = x as any as string; // ОК... но это ужасная идея! // Теперь y имеет тип string, но на самом деле это число! console.log(y.toUpperCase()); // Ошибка выполнения: y.toUpperCase is not a function
Избегайте двойного приведения as any as T. Почти всегда это признак серьезной архитектурной ошибки.
3. Слепая вера в данные извне
Как мы уже discussed ранее, слепо утверждать тип данных, пришедших по сети (из API, localStorage, полей ввода), это прямой путь к runtime-ошибкам.
// Опасно! const userData = JSON.parse(localStorage.getItem('user')) as User; console.log(userData.email); // Может упасть, если в localStorage был другой формат
Всегда проверяйте данные извне! Приведение типов должно применяться только после валидации.
Практические задачи для закрепления
Давайте закрепим материал на практике. Попробуйте решить эти задачи.
Задача 1: Безопасное получение элемента DOM
Напишите функцию getInputValue, которая принимает id элемента и возвращает его значение. Функция должна возвращать string, если элемент найден и это действительно HTMLInputElement и null в противном случае. Используйте приведение типов и проверки.
Решение:
function getInputValue(id: string): string | null { const element = document.getElementById(id); // Проверяем, что элемент существует и является HTMLInputElement if (element instanceof HTMLInputElement) { return (element as HTMLInputElement).value; // Приведение здесь уже излишне, т.к. instanceof проверил тип // Достаточно return element.value; } return null; }
Задача 2: Работа с API ответом
Дан следующий код:
fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(data => { // Здесь data имеет тип any // 1. Создайте интерфейс Todo, который описывает структуру объекта: // { userId: number, id: number, title: string, completed: boolean } // 2. Приведите data к типу Todo с помощью as. // 3. Выведите title и completed в консоль. });
Решение:
interface Todo { userId: number; id: number; title: string; completed: boolean; } fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then((data: any) => { const todo = data as Todo; console.log(`Title: ${todo.title}, Completed: ${todo.completed}`); });
Задача 3: Утверждение const
Создайте read-only объект конфигурации APP_SETTINGS с помощью as const. Объект должен иметь поля: theme: 'dark' | 'light', version: '1.0.0', environment: 'production'. Попробуйте изменить любое поле после объявления и убедитесь, что TypeScript ругается.
Решение:
const APP_SETTINGS = { theme: 'dark' as const, // Можно утвердить каждое поле по отдельности... version: '1.0.0', environment: 'production', } as const; // ...а можно весь объект сразу // APP_SETTINGS.theme = 'light'; // Ошибка: Cannot assign to 'theme' because it is a read-only property. // APP_SETTINGS.version = '2.0.0'; // Ошибка
Заключение
Используйте приведение типов только тогда, когда вы знаете о типе данных больше, чем TypeScript и когда вы не можете перепроектировать код так, чтобы система типов поняла его сама. Всегда предпочитайте правильное объявление типов и дженерики над слепым as.
В следующих уроках мы будем все чаще сталкиваться с ситуациями, где это знание нам пригодится. Мы поговорим о более сложных паттернах, где грамотное использование as и as const делает код и безопасным и выразительным.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


