TypeScript утверждает, что является строго типизированным языком программирования, построенным на основе JavaScript и предоставляющим лучшие инструменты в любом масштабе. Однако TypeScript включает тип any
, который часто может неявно проникнуть в базу кода и привести к потере многих преимуществ TypeScript.
В этой статье рассматриваются способы взять под контроль any
тип в проектах TypeScript. Будьте готовы раскрыть возможности TypeScript, добившись максимальной безопасности типов и улучшив качество кода.
TypeScript предоставляет ряд дополнительных инструментов для улучшения опыта и производительности разработчиков:
Однако как только вы начнете использовать any
тип в своей кодовой базе, вы потеряете все перечисленные выше преимущества. any
тип — это опасная лазейка в системе типов, и его использование отключает все возможности проверки типов, а также все инструменты, которые зависят от проверки типов. В результате теряются все преимущества TypeScript: пропускаются ошибки, редакторы кода становятся менее полезными и многое другое.
Например, рассмотрим следующий пример:
function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any
В приведенном выше коде:
parse
. Когда вы вводите data.
в вашем редакторе вам не будут предложены правильные варианты доступных методов для data
.TypeError: data.split is not a function
поскольку мы передали число вместо строки. TypeScript не может выделить ошибку, поскольку any
отключает проверку типов.res2
также имеет тип any
. Это означает, что однократное использование any
может оказать каскадный эффект на большую часть кодовой базы.
Использовать any
допустимо только в крайних случаях или для нужд прототипирования. В общем, лучше избегать any
использования, чтобы получить максимальную отдачу от TypeScript.
Важно знать об источниках any
типа в базе кода, поскольку явное написание any
— не единственный вариант. Несмотря на все наши усилия избежать использования any
типа, иногда он может неявно проникнуть в базу кода.
В кодовой базе есть четыре основных источника any
типа:
any
в кодовой базе.
Я уже писал статьи « Ключевые аспекты tsconfig» и «Улучшение типов стандартных библиотек» по первым двум пунктам. Пожалуйста, ознакомьтесь с ними, если вы хотите улучшить безопасность типов в своих проектах.
На этот раз мы сосредоточимся на автоматических инструментах для управления появлением any
типа в базе кода.
ESLint — популярный инструмент статического анализа, используемый веб-разработчиками для проверки лучших практик и форматирования кода. Его можно использовать для обеспечения соблюдения стилей кодирования и поиска кода, который не соответствует определенным рекомендациям.
ESLint также можно использовать с проектами TypeScript благодаря плагину typectipt-eslint . Скорее всего, этот плагин уже установлен в вашем проекте. Но если нет, вы можете следовать официальному руководству по началу работы .
Наиболее распространенная конфигурация typescript-eslint
выглядит следующим образом:
module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };
Эта конфигурация позволяет eslint
понимать TypeScript на уровне синтаксиса, что позволяет вам писать простые правила eslint, которые применяются к типам, написанным вручную, в коде. Например, вы можете запретить явное использование any
.
recommended
набор настроек содержит тщательно отобранный набор правил ESLint, направленных на повышение корректности кода. Хотя рекомендуется использовать всю предустановку, для целей этой статьи мы сосредоточимся только на правиле no-explicit-any
.
Строгий режим TypeScript предотвращает использование подразумеваемого any
, но не препятствует any
явному использованию. Правило no-explicit-any
помогает запретить ручное написание any
-либо в кодовой базе.
// ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}
Основная цель этого правила – предотвратить использование any
внутри команды. Это средство укрепления согласия команды с тем, что использование any
в проекте не рекомендуется.
Это важнейшая цель, поскольку даже однократное использование any
из них может оказать каскадное воздействие на значительную часть кодовой базы из-за вывода типов . Однако до достижения максимальной типовой безопасности еще далеко.
Хотя мы уже имели дело с явным использованием any
, в зависимостях проекта все еще существует множество подразумеваемых any
, включая пакеты npm и стандартную библиотеку TypeScript.
Рассмотрим следующий код, который, вероятно, можно увидеть в любом проекте:
const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any
И переменным pokemons
, и settings
неявно был присвоен any
тип. Ни no-explicit-any
ни строгий режим TypeScript не предупредят нас в этом случае. Еще нет.
Это происходит потому, что типы для response.json()
и JSON.parse()
взяты из стандартной библиотеки TypeScript, где эти методы имеют явную аннотацию any
. Мы по-прежнему можем вручную указать лучший тип для наших переменных, но в стандартной библиотеке any
встречается около 1200. Практически невозможно запомнить все случаи, когда any
мог проникнуть в нашу кодовую базу из стандартной библиотеки.
То же самое касается внешних зависимостей. В npm много плохо типизированных библиотек, большинство из которых до сих пор написаны на JavaScript. В результате использование таких библиотек может легко привести к появлению большого количества неявных any
в кодовой базе.
В общем, у any
еще есть много способов проникнуть в наш код.
В идеале нам хотелось бы иметь в TypeScript настройку, которая бы заставляла компилятор жаловаться на любую переменную, получившую any
тип по какой-либо причине. К сожалению, такой настройки в настоящее время не существует и не ожидается.
Мы можем добиться такого поведения, используя режим проверки типов плагина typescript-eslint
. Этот режим работает в сочетании с TypeScript для предоставления полной информации о типах от компилятора TypeScript в правила ESLint. С помощью этой информации можно писать более сложные правила ESLint, которые существенно расширяют возможности TypeScript по проверке типов. Например, правило может найти все переменные any
типа, независимо от того, каким образом any
были получены.
Чтобы использовать правила с учетом типов, вам необходимо немного настроить конфигурацию ESLint:
module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };
Чтобы включить вывод типа для typescript-eslint
, добавьте parserOptions
в конфигурацию ESLint. Затем замените recommended
пресет на recommended-type-checked
. Последний пресет добавляет около 17 новых мощных правил. В рамках данной статьи мы остановимся только на пяти из них.
Правило no-unsafe-argument
ищет вызовы функций, в которых переменная типа any
передается в качестве параметра. Когда это происходит, проверка типов теряется, а также теряются все преимущества строгой типизации.
Например, давайте рассмотрим функцию saveForm
, которая требует объект в качестве параметра. Предположим, мы получаем JSON, анализируем его и получаем any
тип.
// ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.
Когда мы вызываем функцию saveForm
с этим параметром, правило no-unsafe-argument
помечает ее как небезопасную и требует от нас указать соответствующий тип для переменной value
.
Это правило достаточно мощное, чтобы глубоко проверять вложенные структуры данных в аргументах функции. Поэтому вы можете быть уверены, что передача объектов в качестве аргументов функции никогда не будет содержать нетипизированные данные.
// ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });
Лучший способ исправить ошибку — использовать сужение типов TypeScript или библиотеку проверки, например Zod или Superstruct . Например, давайте напишем функцию parseFormValues
, которая сужает точный тип анализируемых данных.
// ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);
Обратите внимание, что разрешено передавать any
тип в качестве аргумента функции, которая принимает unknown
, поскольку при этом не возникает проблем с безопасностью.
Написание функций проверки данных может оказаться утомительной задачей, особенно при работе с большими объемами данных. Поэтому стоит рассмотреть возможность использования библиотеки проверки данных. Например, для Zod код будет выглядеть так:
// ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);
Правило no-unsafe-assignment
ищет назначения переменных, в которых значение имеет any
тип. Такие присваивания могут ввести компилятор в заблуждение, заставив его думать, что переменная имеет определенный тип, тогда как на самом деле данные могут иметь другой тип.
Рассмотрим предыдущий пример анализа JSON:
// ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value
Благодаря правилу no-unsafe-assignment
мы можем перехватить any
тип даже до того, как передать formValues
куда-либо еще. Стратегия исправления остается прежней: мы можем использовать сужение типа, чтобы придать значению переменной определенный тип.
// ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues
Эти два правила срабатывают гораздо реже. Однако, исходя из моего опыта, они действительно полезны, когда вы пытаетесь использовать плохо типизированные сторонние зависимости.
Правило no-unsafe-member-access
запрещает нам доступ к свойствам объекта, если переменная имеет any
тип, поскольку она может иметь значение null
или undefined
.
Правило no-unsafe-call
не позволяет нам вызывать переменную any
типа как функцию, поскольку она может не быть функцией.
Давайте представим, что у нас есть плохо типизированная сторонняя библиотека под названием untyped-auth
:
// ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.
Линтер выделяет две проблемы:
authenticate
может быть небезопасным, поскольку мы можем забыть передать ей важные аргументы.name
из объекта userInfo
небезопасно, поскольку в случае неудачной аутентификации оно будет равно null
.
Лучший способ исправить эти ошибки — рассмотреть возможность использования библиотеки со строго типизированным API. Но если это невозможно, вы можете самостоятельнодополнить типы библиотеки . Пример с фиксированными типами библиотек будет выглядеть так:
// ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }
Правило no-unsafe-return
помогает случайно не вернуть any
тип из функции, которая должна возвращать что-то более конкретное. Такие случаи могут ввести компилятор в заблуждение, заставив его думать, что возвращаемое значение имеет определенный тип, тогда как на самом деле данные могут иметь другой тип.
Например, предположим, что у нас есть функция, которая анализирует JSON и возвращает объект с двумя свойствами.
// ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null
Функция parseForm
может привести к ошибкам во время выполнения в любой части программы, где она используется, поскольку анализируемое значение не проверяется. Правило no-unsafe-return
предотвращает такие проблемы во время выполнения.
Это легко исправить, добавив проверку, гарантирующую, что проанализированный JSON соответствует ожидаемому типу. На этот раз давайте воспользуемся библиотекой Zod:
// ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }
Использование правил с проверкой типов приводит к снижению производительности ESLint, поскольку для вывода всех типов он должен вызывать компилятор TypeScript. Это замедление в основном заметно при запуске линтера в pre-commit хуках и в CI, но не заметно при работе в IDE. Проверка типов выполняется один раз при запуске IDE, а затем типы обновляются по мере изменения кода.
Стоит отметить, что простой вывод типов работает быстрее, чем обычный вызов компилятора tsc
. Например, в нашем последнем проекте с примерно 1,5 миллионами строк кода TypeScript проверка типа с помощью tsc
занимает около 11 минут, в то время как дополнительное время, необходимое для загрузки правил ESLint с учетом типов, составляет всего около 2 минут.
Для нашей команды дополнительная безопасность, обеспечиваемая использованием правил статического анализа с учетом типов, стоит того. В небольших проектах это решение принять еще проще.
Контроль использования any
проектов TypeScript имеет решающее значение для достижения оптимальной безопасности типов и качества кода. Используя плагин typescript-eslint
, разработчики могут выявлять и устранять любые вхождения any
типа в своей кодовой базе, что приводит к созданию более надежной и удобной в обслуживании кодовой базы.
При использовании правил eslint с учетом типов any
появление ключевого слова в нашей кодовой базе будет преднамеренным решением, а не ошибкой или недосмотром. Такой подход ограждает нас от использования any
в нашем собственном коде, а также в стандартной библиотеке и сторонних зависимостях.
В целом, линтер с учетом типов позволяет нам достичь уровня безопасности типов, аналогичного уровню статически типизированных языков программирования, таких как Java, Go, Rust и других. Это значительно упрощает разработку и сопровождение крупных проектов.
Надеюсь, вы узнали что-то новое из этой статьи. Спасибо за чтение!