Три приёма, после которых any почти перестал появляться в коде
as const, satisfies и размеченные объединения. Без теории типов, на примерах статусов, конфигов и состояний загрузки.
any - это не типизация, это её выключатель. Каждый any в коде означает «здесь мы проверим сами». Обычно не проверяем.
Ниже три штуки, которые закрыли большинство мест, где раньше рука тянулась к any или as.
as const
Без него TypeScript расширяет всё до самого широкого типа:
const STATUS = { NEW: 'new', DONE: 'done' };
// { NEW: string; DONE: string } - литералы потеряны
function setStatus(s: 'new' | 'done') {}
setStatus(STATUS.NEW); // ошибка: string не подходит
Дописываем два слова:
const STATUS = { NEW: 'new', DONE: 'done' } as const;
type Status = typeof STATUS[keyof typeof STATUS]; // 'new' | 'done'
setStatus(STATUS.NEW); // ок
Заодно это убирает нужду в enum. Обычный объект даёт тот же союз типов, но остаётся нормальным JS-значением, которое можно перебрать в v-for.
satisfies
Штука, которую я жду с четвёртой версии и до сих пор радуюсь. Классическая дилемма: напишешь аннотацию - потеряешь точность.
type Route = { path: string; roles: string[] };
const routes: Record<string, Route> = {
users: { path: '/users', roles: ['admin'] },
};
routes.orders; // TS молчит, хотя такого ключа нет
satisfies проверяет соответствие типу, но оставляет выведенный конкретный тип:
const routes = {
users: { path: '/users', roles: ['admin'] },
orders: { path: '/orders', roles: ['admin', 'manager'] },
} satisfies Record<string, Route>;
routes.orders.path; // известен
routes.customers; // ошибка компиляции
Я перевёл на это все словари: карты роутов, темы, переводы, маппинги статус → цвет. Опечатку в ключе теперь ловит компилятор, а не тестировщик за день до релиза.
Размеченные объединения
Тип, который есть в любом проекте:
interface State {
loading: boolean;
data?: User[];
error?: string;
}
Он разрешает бессмыслицу: loading: true одновременно с data и error. Дальше в шаблоне появляется v-if="!loading && !error && data", и однажды кто-то забудет одну проверку.
Размечаем по полю:
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
function render(state: State) {
switch (state.status) {
case 'success': return state.data.length; // data точно есть
case 'error': return state.error;
case 'loading': return '...';
case 'idle': return '';
}
}
Невалидное состояние теперь невозможно выразить. А если добавить вот такую функцию в default, то при появлении нового статуса компилятор напомнит про забытую ветку:
function assertNever(x: never): never {
throw new Error(`Необработанный вариант: ${JSON.stringify(x)}`);
}
А если тип реально неизвестен
Тогда unknown, а не any. Разница простая: с any можно делать что угодно, с unknown - ничего, пока не проверишь.
const data: unknown = JSON.parse(raw);
data.name; // не даст
if (typeof data === 'object' && data && 'name' in data) {
// а тут уже можно
}
Для всего, что приходит извне - ответ API, localStorage, query-параметры - лучше сразу гнать через схему (Zod, Valibot, любая). Тогда тип не обещан, а проверен в рантайме.
Аргумент, который в спорах работает лучше всего: типы - это тесты, которые пишешь один раз, а выполняются они на каждом сохранении файла. any выключает их ровно там, где данные приходят снаружи. То есть там, где ломается чаще всего.
У меня на ревью правило: any можно, но с комментарием, почему тип невыразим. За полгода такие комментарии появились раза три, всё остальное закрылось unknown и союзами.