
От переводчика
Привет, друзья! Меня зовут Максим Иванов и сегодня поговорим о том, почему использование any и расплывчатых типов вроде Function — это не быстрое решение, а мина замедленного действия. Ведь вы лишаете себя главной суперсилы TypeScript — надежности типов!
Содержание
Введение
Тип any в TypeScript по своей сути является злом и небезопасной особенностью встроенной по умолчанию в систему типов языка. Данный тип дает возможность разработчику записать такое значение, которое может быть чем угодно, использоваться где угодно и как угодно вместо любого другого типа данных.
function fn(s) {
console.log(s.subtr(3));
}
fn(42); // ⚠️ Ошибка в рантайме
Использование any небезопасно, поскольку таким образом вы отключаете многие проверки во время компиляции вашего кода в JavaScript, что по итогу ограничивает способность TypeScript помогать вам, отказываясь тем самым от преимуществ строгой типизации.
Линтеры — это не только важнейший инструмент для поддержания качества кода на высоком уровне, но и важный помощник в борьбе с any. Пакет typescript-eslint содержит несколько таких правил, которые помогают предотвратить небезопасные практики еще до начала компиляции, связанные с типом any. Эти правила обнаруживают как прямые использования any, так и целые паттерны кода, которые незаметно его допускают. В этой публикации мы разберемся с тем, что конкретного делают эти правила, а также рассмотрим несколько полезных способов предотвратить проникновение any в наш код.
noImplicitAny - уже хорошо, но недостаточно
TypeScript предоставляет флаг и настройку noImplicitAny для сообщений о ситуациях, при которых для значения не может быть выведен тип лучше, чем any.
Без noImplicitAny:
// $ tsc fn.ts
function fn(s) { // ⚠️ s неявно получает тип `any`, не будет ошибки компиляции
console.log(s.subtr(3));
}
С включенным noImplicitAny:
// $ tsc fn.ts --noImplicitAny
function fn(s) { // ❌ ОШИБКА: Parameter 's' implicitly has an 'any' type
console.log(s.subtr(3));
}
Как правило noImplicitAny включен по умолчанию, если вы до этого также включили strict режим, который рекомендуется для всех проектов на TypeScript. Однако, даже при включенном noImplicitAny, тип any все равно может легко проникать в вашу кодовую базу.
noImplicitAny не запрещает использовать явное указание any в аннотациях типов к вашим переменным или параметрам методов, функций. Даже некоторые встроенные API, такие как JSON.stringify, Function, по умолчанию скрывают за собой тип any.
Вот несколько наглядных примеров, где встроенное API создает серьезные проблемы в TypeScript на ровном месте:
interface User {
id: number;
name: string;
email: string;
}
const user: User = {
id: 1,
name: "John",
email: "john@example.com"
};
// Теперь jsonString имеет тип any - вся типобезопасность потеряна
const jsonString: any = JSON.stringify(user);
// parsed теперь any - компилятор не знает о его структуре
const parsed: any = JSON.parse(jsonString);
// ⚠️ Не будет ошибки компиляции
console.log(parsed.nonExistentProperty);
// ⚠️ Может runtime ошибка, если name не строка
console.log(parsed.name.toUpperCase());
// ⚠️ TypeScript не предотвратит это
const unsafeData: any = JSON.parse('{"malicious": "code"}');
// Теперь у нас User с неправильной структурой
const injected: User = unsafeData;
// fn теперь имеет тип Function - мы ничего не знаем о сигнатуре
const fn: Function = () => {};
// Компиляция проходит
// ⚠️ Но любые аргументы и любое количество - все разрешено!
fn(1, 2, 3);
fn("hello", "world");
fn();
function executeCallback(callback: Function) {
const result = callback("hello", 123);
// Мы не знаем, что вернет callback и какие аргументы ожидает
// ⚠️ Может быть runtime error
console.log(result.toUpperCase());
}
По итогу:
Полная потеря типобезопасности
Распространение any по кодовой базе
Runtime ошибки, которые TypeScript не может поймать
Ложное чувство безопасности
И все же, включение noImplicitAny это первый шаг к повышению типобезопасности вашего проекта, так как он заставляет разработчиков явно думать о типах данных, что приводит к более продуманной архитектуре и предсказуемому поведению.
Запрещаем явное указание небезопасных типов
Нашей первой линией обороны против any является добавление правил линтинга, которые будут оповещать о том, что кто-то явно прописал их в коде. Разумеется, разработчики всегда могут отключить любые ESLint правила с помощью специальных комментариев, поэтому даже линтеры не гарантируют полного предотвращения указания небезопасных типов таких как any, но это уже на совести самих разработчиков.
1. Не используем any (unsafe тип)
// eslint.config.ts
//...
export default tseslint.config({
rules: {
"@typescript-eslint/no-explicit-any": "error"
}
});
@typescript-eslint/no-explicit-any оповещает о каждом случае использования any в вашем коде. Это помогает предотвратить его использование и заставить вас найти более безопасный тип. Такой код считался бы некорректным еще до начала компиляции:
function fn(s: any) {
// ~~~
// Тут на помощь пришел бы ESLint ❌
// eslint(@typescript-eslint/no-explicit-any):
// Unexpected any. Specify a different type.
console.log(s.subtr(3));
}
В данном случае, вместо any более типобезопасно использовать string.
Вместо any используйте unknown
В каких-то случаях, вы не всегда можете заранее знать, что за тип данных может вам прилететь, особенно, когда вы работаете с динамическими данными. Вот для таких случаев, был придуман unknown .
Тип unknown был введен в TypeScript версии 3.0, которая вышла в июле 2018 года. Он появился в качестве типобезопасной альтернативы для any.
┌────────────┐
│ any │ // «верхний и нижний» для всех сразу
└─────┬──────┘
│
┌─────────┴─────────┐
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ unknown │ │ never │
└─────┬──────┘ └────────────┘
│
┌──────────┼───────────┐
│ │ │
┌──────▼───┐ ┌────▼────┐ ┌────▼────┐
│ object │ │ string │ │ number │ ... (и другие типы данных)
└──────────┘ └─────────┘ └─────────┘
Как мы видим из этой схемы, any — особый тип: может быть чем угодно, отключает систему типов. Стоит на самом верху (супертип) и одновременно на самом низу (сабтип), потому что совместим с любым типом. Он опасен тем, что таким образом, вы говорите компилятору: "Не беспокойся, я сам все проверю", и компилятор вас слушается. Вы можете делать со значением что угодно, и компилятор не будет вас останавливать — даже если вы совершите ошибку, что может привести к непредсказуемым последствиям в рантайме.
let value: any = "Hello";
let s1: string = value;
let n1: number = value;
let result1 = value.toFixed(2); // ⚠️
// Скомпилируется, но так как у строки нет метода toFixed,
// нас ждет ошибка в runtime
unknown более безопасный, так как, чтобы использовать значение вам нужно сначала сделать type guard проверку, как раз из-за того, что переменная после этого типа может содержать значение любого другого (string, number, User).
let value: unknown = "Hello";
let s1: string = value; // ❌ Type 'unknown' is not assignable to type 'string'
let n1: number = value; // ❌ Type 'unknown' is not assignable to type 'number'
let result1 = value.toFixed(2); // ❌ Object is of type 'unknown'
Как мы видим, на каждой строчке, мы можем получить ошибку компиляции. Чтобы правильно работать с динамическими и неизвестными данными, вы должны сначала его "сузить" (narrow down), указав что это за тип:
let value: unknown = "Hello";
if (typeof value === "string") {
// Теперь TypeScript *знает*, что здесь value — это string
let s2: string = value; // ✅
console.log(value.toUpperCase()); // ✅
}
if (typeof value === "number") {
let n2: number = value; // ✅
let result2 = value.toFixed(2); // ✅
}
Чтобы быстро заменить any в вашем коде на unknown, вы можете использовать флаг fixToUnknown из правила @typescript-eslint/no-explicit-any:
// eslint.config.ts
//...
export default tseslint.config({
rules: {
"@typescript-eslint/no-explicit-any": [
"error",
{
fixToUnknown: true
}
]
}
});
2. Не используем Function (unsafe тип)
@typescript-eslint/no-unsafe-function-type сообщает о любом использовании встроенного типа Function в вашем коде. Тип Function является нестрогим и небезопасным. Он позволяет вызывать функцию с любым количеством аргументов и возвращает тип any.
Почему все же Function существует в TypeScript, если он небезопасный? Главная причина - обратная совместимость. TypeScript построен поверх JavaScript, а в JavaScript функции — это объекты первого уровня, по сути Function супертип, который является фундаментальной и встроенной частью языка. TypeScript должен уметь описывать существующую экосистему JavaScript, включая нативные API браузера и сторонние библиотеки, которые широко используют функции в общем виде.
Возьмем, к примеру, следующую версию функции greet, которая принимает в качестве аргумента функцию. Тип Function не описывает, ни какие параметры есть у такой функций, ни какое возвращаемое значение может быть. Такой код считался бы некорректным еще до начала компиляции:
function greet(getFriend: Function) {
// ~~~~~~~~
// Тут на помощь пришел бы ESLint ❌
// eslint(@typescript-eslint/no-unsafe-function-type):
// The `Function` type accepts any function-like value.
// Prefer explicitly defining any function parameters and return type.
console.log(`Hello, ${getFriend().toUpperCase()}!`);
}
greet(() => 'Lazlo'); // ✅
// Должна быть ошибка, но код скомпилируется
greet(() => ({ name: 'Nadya' })); // ⚠️
В данном случае, вместо Function более типобезопасно использовать () => string.
3. Приведение к типу unknown в выбрасываемых исключениях
В TypeScript мы перехватываем ошибки с помощью catch (error). Исторически сложилось, что TypeScript определял тип у переменной error как any. Это небезопасно, потому что:
Вы теряете проверку типов;
Компилятор позволяет вам делать с error, что угодно, хоть вызывать error.message, error.someFakeProperty, error.toUpperCase(), даже если у реальной ошибки таких свойств и методов нет. Это приводит к runtime-ошибкам.any распространяется дальше по коду.
Использование any ослабляет всю систему типов.
Чтобы избежать наследуемой природы JavaScript, в TypeScript подобно флагу noImplicitAny, вы можете использовать флаг useUnknownInCatchVariables. Он изменяет тип переменных в блоке catch на unknown.
Было
try {
// ... какой-то код, который может упасть
} catch (error) { // ⚠️ Плохо! Тип 'any' по умолчанию
console.log(error.message);
}
Стало
try {
// ... какой-то код, который может упасть
} catch (error: unknown) { // ✅ Хорошо! Тип 'unknown'
// console.log(error.message); У объекта типа 'unknown' нет свойства 'message'
// Теперь мы обязаны проверить тип ошибки перед работой с ней
if (error instanceof Error) {
console.log(error.message); // Теперь безопасно
} else {
console.log('Произошла неизвестная ошибка', error);
}
}
Однако, этот флаг никак не реагирует на встроенное API браузеров, где по умолчанию у Promise (промисов) в методе .catch() аргументом ошибки является тип any по умолчанию.
Вам поможет правило @typescript-eslint/use-unknown-in-catch-callback-variable, которое принудительно требует всегда использовать тип unknown в качестве типа у аргумента. Например, в следующем коде TypeScript не сообщит об ошибке типа, но зато линтер не даст пройти такому коду:
function rejectWith(value: string) {
return Promise.reject(value);
}
rejectWith('Nandor').catch(error => {
// ~~~~~
// Тут на помощь пришел бы ESLint ❌
// eslint(@typescript-eslint/use-unknown-in-catch-callback-variable):
// Prefer the safe `: unknown` for a `catch` callback variable.
// Должна быть ошибка, но код скомпилируется
console.log(error.message); // ⚠️
});
Запрещаем использовать небезопасные типы
Как только тип any уже появляется в нашем коде, он становится «заразным». он может превращать типы связанных с ним значений в такие же any.
Пакет typescript-eslint (52+ млн скачиваний) также включает набор таких правил, которые помечают определенные участки кода небезопасными. Каждое из следующих правил сообщает о конкретном способе отказаться от any:
@typescript-eslint/no-unsafe-argument — запрещает использование any в качестве аргументов при вызове функций;
@typescript-eslint/no-unsafe-assignment — запрещает присваивать any к переменным и свойствам класса;
@typescript-eslint/no-unsafe-call — запрещает вызывать переменную или свойство класса типа any как функцию;
@typescript-eslint/no-unsafe-member-access — запрещает доступ к свойствам класса с типом any;
@typescript-eslint/no-unsafe-return — запрещает использовать any как возвращаемое значение из функции.
Например, далее функция parseData имеет ряд нарушений с точки зрения линтера. В ней мы присваиваем в переменную shape данные с типом any и работаем с ней будто это объект типа Shape:
export interface Shape {
label: string;
value: number;
}
export function parseShapeFromData(raw: string): Shape {
const shape = JSON.parse(raw);
// ~~~~~~~~~~~~~~~~~~~~~~~
// Тут на помощь пришел бы ESLint ❌
// eslint(@typescript-eslint/no-unsafe-assignment):
// Unsafe assignment of an `any` value.
console.log('Making a shape with value:', shape.value);
// ~~~~~
// Тут на помощь пришел бы ESLint ❌
// eslint(@typescript-eslint/no-unsafe-member-access):
// Unsafe member access .value of an `any` value.
return shape;
// Тут на помощь пришел бы ESLint ❌
// eslint(@typescript-eslint/no-unsafe-return):
// Unsafe return of an `any` value.
}
Если бы функция parseShapeFromData включала бы type guard проверки shape или использовала библиотеку для валидации, такую как Zod, то во время выполнения стало бы ясно, соответствуют ли данные ожидаемому типу Shape.
В совокупности правила @typescript-eslint/no-unsafe-*, обеспечивают безопасную работу при работе с any, так как предоставляют целый набор проверок, которые помогают отлавливать большинство случайных использования небезопасных типов в кодовой базе. Авторы пакета typescript-eslint настоятельно рекомендуют использовать эти правила вместе с флагом noImplicitAny в TypeScript.
Дополнительные советы
1. Правильно работаем с отключениями проверок TypeScript
Тип any не единственный способ обойти систему типов в TypeScript. Разработчики также могут использовать встроенные директивы в коде, такие как: // @ts-expect-error и // @ts-ignore. Эти комментарии заставляют TypeScript игнорировать ошибки на строке. Иногда отключение TypeScript на уровне строки действительно бывает нужно, но это всегда небезопасно и не должно стать вашей повседневной практикой.
// @ts-expect-error и // @ts-ignore почти одинаковы, но первый выдаст ошибку, если на строке вообще не было ошибки, которую нужно скрывать. Поэтому обычно лучше использовать // @ts-expect-error, а не // @ts-ignore.
Правило @typescript-eslint/ban-comment помогает отслеживать такие случаи. По умолчанию оно:
запрещает все директивы @ts-ignore и @ts-nocheck;
запрещает @ts-expect-error, если рядом нет пояснительного комментария;
требует, чтобы пояснительный комментарий содержал хотя бы 3 символа.
Допустим из пакета @example/package мы импортируем функцию processString, которая принимает строку, но ее типы ошибочно указан, что она принимает число. Тогда можно было временно использовать // @ts-expect-error:
❌ Было
import { processString } from '@example/package';
// @ts-ignore
processString('Москва');
✅ Стало
import { processString } from '@example/package';
// @ts-expect-error — неверный тип в @example/package
processString('Москва');
Как только разработчики пакета @example/package исправят ошибку и поменяют тип, комментарий сам станет ошибкой и напомнит удалить его.
2. Плагин ESLint Comments
У ESLint есть свои встроенные директивы для отключения правил линтинга: // eslint-disable, // eslint-disable-next-line . Они могут отключать даже правила, о которых мы говорили выше. Иногда такие комментарии нужны, если правило или система типов TypeScript неправильно интерпретируют код.
В таких случаях полезно использовать плагин @eslint-community/eslint-plugin-eslint-comments. Его правило настроено следующим образом:
disable-enable-pair — не дает случайно отключить правила для всего файла;
no-unlimited-disable — не дает отключить все правила сразу;
require-description — требует объяснить, зачем вы отключаете правило.
Предположим, разработчики пакета @example/package поменяли аргументу тип на any , чтобы выполнить рефакторинг и не блокировать других разработчиков. Логично, что в таком случае, у нас посыпятся ошибки от использования @typescript-eslint/no-unsafe-call в нашей кодовой базе. Можно отключить правило, но тогда давайте в таком случае оставим комментарий, который должен объяснять, почему мы это сделали, и желательно ссылаться на задачу в бэклоге проекта.
❌ Было
import { processString } from '@example/package';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
processString('Москва');
✅ Стало
import { processString } from '@example/package';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call -- временно any, см. JIRA-123
processString('Москва');
Когда типы будут исправлены, запуск ESLint с флагом reportUnusedDisableDirectives выдаст предупреждение удалить этот комментарий.
3. ts-reset
Как мы уже знаем any может прилететь из от встроенных глобальных функций, браузерного API. Это может быть, JSON.parse(), метод .json() у Promise и свойства какого-нибудь объекта Storage. Помним, что это сделано для обратной совместимости с JavaScript, который является слаботипизированным языком.
Библиотека ts-reset переписывает глобальные типы на более безопасные, заменяя any на unknown:
const data = JSON.parse(`"clearly-a-string"`);
// ^? any (без ts-reset)
// ^? unknown (с ts-reset)
console.log(data.some.property.that.does.not.exist);
Помните, что ts-reset применяется глобально, поэтому использовать его стоит только в приложениях, а не в поставляемых в npm библиотеках.
Заключение
По итогу, что такое слабая типизация. Представьте себе такую ситуацию. Вы даете другу сумку и говорите: "Подержи, пожалуйста, там хрупкое". А он бросает ее на диван, хлопает по ней рукой и кричит: "Не волнуйся, там все в порядке!". any в TypeScript — это именно такой друг. Вы говорите системе: "Вот переменная, доверься мне, там будет что-то". А TypeScript, который готов был защищать ваш код от ошибок, вынужден снять каску и сказать: "Ладно, сам разбирайся".
Поэтому авторы пакета typescript-eslint настоятельно рекомендуют использовать использовать хотя бы пресет tseslint.configs.recommendedTypeChecked в вашей конфигурации для ESLint. Этот пресет включает правила no-explicit-any и no-unsafe-*, о которых говорилось выше, а также множество других правил для повышения типобезопасности в вашем проекте. Но если хотите еще более строгую проверку используйте tseslint.configs.strictTypeChecked, он включает все рекомендованные правила, а также более жесткие, например use-unknown-in-catch-callback-variable и другие.
Почему разработчики все еще используют any?
Использование any в проектах, как правило, это сознательный компромисс, а не всегда "плохая практика". Вот основные причины, почему его используют:
Быстрое прототипирование - чтобы не тратить время на типы
Тесты и моки - где строгая типизация излишня
Миграция с JavaScript - постепенное добавление типов
Историческое наследие, боязнь внедрить breaking changes, нехватка ресурсов и времени на рефакторинг
Например, в таких репозиториях как Angular от Google, TypeScript от Microsoft, React от Facebook, или наших ребят Taiga UI от Т-Банка и Gravity UI от Яндекса вы обнаружите использование any. Где-то они используются по делу, чтобы не добавлять излишней строгости, а где-то какие-то куски кода уже много лет просто не переписывались и руки не доходили.
Комментарии (0)
lear
20.09.2025 20:00Пока что самое напрягающее для меня место в typescript это обработка ошибок.
Некоторые либы вообще не наследуют ошибки от Error, а просто выбрасывают кастомный объект с описанием ошибки. Но проблема в том, что об этом нельзя никак узнать кроме чтения кода полностью, ну или пока в рантайме не появится какая-то белиберда вместо ошибки...
DmitryOlkhovoi
Как бы расстаться с Typescript, не тратить время на описание типов
ganqqwerty
Нравится читать код внутри функций?
lear
Эмм, а зачем расставаться с typescript?
Чтобы как раньше открывать 100500 доков для разных либ и узнавать какие есть функции, что они принимают и что отдают?
Мне как-то легче описание функций смотреть в IDE, а не тратить время на то, чтобы найти доку по определенной либе.
PS. Я не имею ввиду начальное знакомство с либой, это именно про этап взаимодействия с либой.
Egor_Grin
да что уж там, если расставаться - так сразу с разработкой. там алгоритмы описывать надо