paint-brush
Делаем TypeScript по-настоящему «строго типизированным»к@nodge
17,593 чтения
17,593 чтения

Делаем TypeScript по-настоящему «строго типизированным»

к Maksim Zemskov12m2023/09/10
Read on Terminal Reader
Read this story w/o Javascript

Слишком долго; Читать

TypeScript предоставляет тип «Любой» для ситуаций, когда форма данных заранее неизвестна. Однако чрезмерное использование этого типа может привести к проблемам с безопасностью типов, качеством кода и опытом разработчиков. В этой статье рассматриваются риски, связанные с типом «Любой», определяются потенциальные источники его включения в базу кода и предлагаются стратегии контроля его использования на протяжении всего проекта.

People Mentioned

Mention Thumbnail
featured image - Делаем TypeScript по-настоящему «строго типизированным»
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript утверждает, что является строго типизированным языком программирования, построенным на основе JavaScript и предоставляющим лучшие инструменты в любом масштабе. Однако TypeScript включает тип any , который часто может неявно проникнуть в базу кода и привести к потере многих преимуществ TypeScript.


В этой статье рассматриваются способы взять под контроль any тип в проектах TypeScript. Будьте готовы раскрыть возможности TypeScript, добившись максимальной безопасности типов и улучшив качество кода.

Недостатки использования Any в TypeScript

TypeScript предоставляет ряд дополнительных инструментов для улучшения опыта и производительности разработчиков:


  • Это помогает обнаружить ошибки на ранних стадиях разработки.
  • Он предлагает отличное автодополнение для редакторов кода и IDE.
  • Он позволяет легко проводить рефакторинг больших баз кода с помощью фантастических инструментов навигации по коду и автоматического рефакторинга.
  • Он упрощает понимание базы кода, предоставляя дополнительную семантику и явные структуры данных через типы.


Однако как только вы начнете использовать 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 типа:

  1. Параметры компилятора в tsconfig.
  2. Стандартная библиотека TypeScript.
  3. Зависимости проекта.
  4. Явное использование any в кодовой базе.


Я уже писал статьи « Ключевые аспекты tsconfig» и «Улучшение типов стандартных библиотек» по первым двум пунктам. Пожалуйста, ознакомьтесь с ними, если вы хотите улучшить безопасность типов в своих проектах.


На этот раз мы сосредоточимся на автоматических инструментах для управления появлением any типа в базе кода.

Этап 1. Использование ESLint

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 еще есть много способов проникнуть в наш код.

Этап 2. Расширение возможностей проверки типов

В идеале нам хотелось бы иметь в 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 и других. Это значительно упрощает разработку и сопровождение крупных проектов.


Надеюсь, вы узнали что-то новое из этой статьи. Спасибо за чтение!

Полезные ссылки