TypeScript для JS-разработчика: приёмы из практики
Вы знаете JavaScript, но TypeScript в проде разочаровывает: any расползается, типы дублируют рантайм, сборка тормозит. Разбираем приёмы, которые убирают лишний код и ловят ошибки до деплоя.

Вы переносите проект с JavaScript на TypeScript, включаете компилятор — и получаете сотни ошибок, половину которых закрываете через any. Через месяц типов формально много, а пользы от них мало: сигнатуры расходятся с реальными данными, а @ts-ignore висит над каждым третьим файлом. Ниже — приёмы, которые дают типам работать на вас, а не наоборот. Всё проверено на TypeScript 5.x.
Проблема номер один: any вместо unknown
Привычка из JS — не думать о типе внешних данных. В TS это выливается в any, который отключает проверки не только на переменной, но и на всём, что от неё зависит. Один any в ответе API — и весь код обработки этого ответа компилятор перестаёт проверять.
Правильная замена — unknown. Он говорит «тип неизвестен», но заставляет проверить его перед использованием.
// Плохо: тип response — any, ошибку в data.usr компилятор пропустит
async function loadUser(id: string) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json(); // any
return data.name; // опечатку data.nme никто не поймает
}
// Хорошо: json() возвращаем как unknown и валидируем
function isUser(value: unknown): value is { id: string; name: string } {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
async function loadUserSafe(id: string) {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isUser(data)) {
throw new Error("Неожиданный формат ответа /api/users");
}
return data.name; // здесь тип уже сужен, автодополнение работает
}
Цена: писать функции-предикаты (value is T) руками скучно. На больших схемах это заменяют библиотекой валидации — об этом ниже в таблице.
Не дублируйте типы: выводите их
Частая ошибка новичка в TS — писать interface для того, что компилятор выведет сам. Дублирование расходится с кодом при первом же рефакторинге.
typeof и ReturnType вместо ручных интерфейсов
// Плохо: конфиг и интерфейс живут отдельно и разъезжаются
interface Config {
host: string;
port: number;
retries: number;
}
const config: Config = { host: "localhost", port: 5432, retries: 3 };
// Хорошо: тип выводится из значения, дублирования нет
const appConfig = {
host: "localhost",
port: 5432,
retries: 3,
} as const;
type AppConfig = typeof appConfig; // { readonly host: string; ... }
as const1 делает поля readonly и сужает литералы: port получит тип 5432, а не number. Для строковых значений это особенно ценно — вместо string вы получите точный набор допустимых строк.
Тип из объединения ключей
const roles = {
admin: "Администратор",
editor: "Редактор",
viewer: "Наблюдатель",
} as const;
type Role = keyof typeof roles; // "admin" | "editor" | "viewer"
function assignRole(role: Role) {
// Передать "guest" сюда компилятор не даст
return roles[role];
}
Discriminated unions вместо флагов и опциональных полей
Ещё одна привычка из JS — объект, где половина полей опциональна в зависимости от состояния. В TS это порождает бесконечные проверки на undefined.
// Плохо: непонятно, какие поля заполнены при error
interface RequestState {
loading: boolean;
data?: string[];
error?: string;
}
// Хорошо: размеченное объединение по полю status
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };
function render(state: RequestState) {
switch (state.status) {
case "success":
return state.data.join(", "); // data гарантированно есть
case "error":
return state.error; // error гарантированно есть
default:
return "";
}
}
Если состояние объекта нельзя описать одним булевым флагом — почти наверняка это размеченное объединение. ФлагиisLoading,isError,isEmptyрядом друг с другом допускают комбинации, которых в реальности не бывает.
Валидация: рантайм и типы из одного источника
TypeScript исчезает при компиляции — он не проверяет данные во время работы программы. Ответ сервера, тело запроса, содержимое localStorage могут не совпасть с типом, и компилятор об этом не узнает. Решают это библиотекой рантайм-валидации, которая заодно выводит статический тип.
| Подход | Проверка в рантайме | Дублирование типа | Когда брать |
|---|---|---|---|
| Ручной type guard | Да, пишете сами | Да, руками | Одна-две простые формы |
Интерфейс + приведение as | Нет | Нет | Только внутренние данные, которым доверяете |
| Библиотека схем (Zod и подобные) | Да | Нет, тип выводится из схемы | Внешние данные: API, формы, конфиги |
Идея схемной библиотеки: вы описываете форму данных один раз, из неё получаете и валидатор, и статический тип. Проверьте актуальный синтаксис и имена методов в документации выбранной библиотеки — они менялись между мажорными версиями. Общий принцип: схема описывает поля, метод разбора возвращает либо валидные данные, либо ошибку, а тип извлекается служебной утилитой из схемы.
Строгий режим включайте сразу
Флаг strict в tsconfig.json включает набор проверок разом — в том числе strictNullChecks, из-за которого null и undefined перестают молча подставляться куда угодно. На новом проекте включайте с первого коммита: добавить строгость к готовому коду в разы больнее.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
Отдельно отмечу noUncheckedIndexedAccess: он добавляет undefined к результату доступа по индексу (arr[i], obj[key]). Это ловит частый баг, когда вы обращаетесь к элементу, которого в массиве нет. В strict он не входит — включайте отдельно.
Точный состав флагов, входящих в strict, сверяйте в официальной документации TSConfig: список пополняется от версии к версии.
Типичные ошибки, на которых спотыкаются
asвместо проверки. Приведениеvalue as User— это обещание компилятору, а не проверка. Если данные внешние, обещание нарушится в рантайме без единой ошибки на этапе сборки.enumбез нужды. Числовыеenumкомпилируются в объект и попадают в бандл. Для набора строк чаще достаточноas constиkeyof typeof— без рантайм-следа.- Типы из библиотеки в рантайме.
import typeудаляется при сборке. Если вы импортировали значение черезimport type, оно не окажется в бандле — а ошибка вылезет только при запуске. Functionи пустой{}как типы.{}означает «любое не-null значение», а не «пустой объект». Для реального объекта используйтеRecord<string, unknown>или конкретную форму.
Что делать дальше
Порядок внедрения, который меньше всего мешает работе: включите strict на новом коде, замените все any во входных данных на unknown с валидацией, переведите объекты-состояния на размеченные объединения. Утилитные типы (Pick, Omit, Partial, Record) осваивайте по мере надобности, а не заучивая списком. Как ориентир по возможностям языка держите под рукой раздел Handbook на официальном сайте TypeScript — он обновляется вместе с релизами.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: TypeScript Handbook (официальная документация), TSConfig Reference — compilerOptions
Частые вопросы
У меня TypeScript 4.x, а не 5.x — приёмы сработают?
Обязательно ли включать strict, если проект уже большой?
Зачем валидировать данные, если TypeScript уже проверяет типы?
any и unknown — в чём практическая разница?
Не сработало: компилятор всё равно ругается после сужения типа
Замедлит ли TypeScript сборку в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.