Как я писал плагин для TypeScript. Часть 1. IDE

от автора

Привет, Хабр! Меня зовут Дима, я Head of Frontend в Dodo Engineering. Моя команда создаёт инструменты для удобной работы с фронтендами, унифицирует подходы к разработке, помогает другим командам в создании удобных пользовательских интерфейсов Dodo IS.

Недавно мне срочно понадобилось написать плагин для TypeScript. Начал я с того, что погуглил, как это сделать. По пути боролся с повышенным потреблением памяти и искал недостающие файлы в массиве, переписывал Proxy и не только, а закончил на… А впрочем это вы узнаете в конце.

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

Почему решил написать?

TypeScript — странный язык. Он помогает разработчикам, но при этом вызывает много вопросов. Он добавляет статическую типизацию в JavaScript, что позволяет IDE заранее «ловить» ошибки и подсказывать разработчику, как лучше писать код. Это делает код более предсказуемым и помогает избежать множества багов на этапе написания.

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

Кроме того, TypeScript — это как дополнительный слой в разработке. Сам по себе он не может работать в браузере или на сервере, его сначала нужно преобразовать в обычный JavaScript. И хотя TypeScript помогает писать более чистый код, в итоге весь процесс выглядит лишним шагом, причём не всегда оправданным.

Тем не менее TypeScript в каком-то смысле является дополнением к тестам и документации, особенно в больших проектах, но только если его правильно настроить. Одна из самых важных настроек в TypeScript — опция strict. Она заслуживает особого внимания, когда речь идёт о масштабных проектах. Эта опция включает в себя несколько других.

strictNullChecks

Одна из самых главных опций. Без неё все | null | undefined (nullable) типы просто игнорируются:

function findUserName(): string | null {   return null; // здесь может быть обращение к бэкенду }  const userName = getUserName();  console.log(userName.toUpperCase()); // без включённого strict мы не увидим ошибку в IDE, хотя userName может оказаться null. А вот в рантайме упадём с ошибкой 

noImplicitAny

function stringToUpper(name) { // с включённым strict TypeScript не позволит передать любой аргумент в name, запретит неявный any-тип   return name.toUpperCase(); }  stringToUpper(42); // здесь мы упадём с ошибкой в рантайме 

Существует ряд других опций, которые также критичны, но требуют более детального рассмотрения. Чтобы не увеличивать объём статьи, я приведу их список со ссылками на официальную документацию:

Но что делать, если на этапе зарождения проекта strict не был включён? Ведь по началу может казаться, что это лишняя нагрузка: проект небольшой, всё под контролем, и строгая типизация кажется избыточной. К тому же переводить проект с JavaScript на TypeScript намного проще с выключенным strict — просто переименовать .jsфайлы в .ts.

Но когда проект вырастает до значительных размеров, исправлять эти ошибки становится всё сложнее. И если команда решит на каком-то этапе включить strict, она столкнётся с тем, что нужно переписывать огромные участки кода, чтобы удовлетворить требованиям строгой типизации. Это может сильно затормозить разработку и усложнить поддержку кода.

С такой проблемой я и столкнулся на одном из проектов. Несколько лет назад мы перевели его с JavaScript на TypeScript, не включив опцию strict. Возможно, мы сделали это случайно — в пустом tsconfig она отключена по умолчанию, а возможно осознанно — из-за описанных выше причин. Так или иначе, команда начала сталкиваться с проблемами — в проде начали вылезать ошибки, связанные со strictNullChecks. Дело было в переменных. В коде они были нулевыми, но в какой-то момент nullable терялся, так как IDE и TypeScript никак об этом не предупреждали.

В проекте больше 3000 файлов. Простое включение strict подсветило сотни ошибок. Их исправление заняло бы недели, а может и месяцы. Возник вопрос: а как перевести только часть проекта, содержащую критичную функциональность? К сожалению, TypeScript не позволяет переопределять tsconfig для определённых папок, так как он воспринимает весь код как целостный проект, где друг с другом связан каждый файл и каждый тип. У TypeScript есть issue на эту тему, открытый в 2019 году.

Одно из решений: раздробить один монолит-проект на мелкие части, workspaces. И уже в этих частях внедрять strict. Но из-за тесной связанности компонентов проекта, эта задача заняла бы ещё больше времени. Хоть мы и смогли собрать низко висящие фрукты — выделили независящие от других частей проекта утилиты и UI-компоненты — этого всё равно было мало. Оставалась потребность переопределять tsconfig только для определённых частей проекта и постепенно переводить на strict.

В какой-то момент я вспомнил о возможности писать свои плагины для TypeScript. И пошёл разбираться…

Плагины для IDE

По запросу «TypeScript plugin» в гугле, мы попадаем на официальный гайд по написанию плагина. Это не самый подробный гайд. Он задаёт только базовый вектор в создании плагинов. Поэтому я как слепой котёнок вооружился исходниками TypeScript, IDE, и пошёл писать плагин.

Быстрый набросок плагина

Первым делом я создал два workspace. В одном из них находится код моего плагина (packages/plugin), а в другом (packages/example) — этот плагин подключается как обычный npm-пакет. Весь код плагина содержится в index.ts файле и собирается обычной командой tsc. В example добавляем в tsconfig следующие настройки:

{   "compilerOptions": {     "plugins": [       {         "name": "ts-plugin", // название workspace в packages/plugin/package.json       },     ]   }, } 

Самый простой код плагина может выглядеть так:

import type ts from 'typescript/lib/tsserverlibrary';  const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({     create: pluginCreateInfo =>         new Proxy(pluginCreateInfo.languageService, {             get: (target, property: keyof ts.LanguageService) => {                 if (property === `getSemanticDiagnostics`) {                     return (fileName: string) => {                         const originalDiagnostic = target.getSemanticDiagnostics(fileName);                          return originalDiagnostic.map(diagnostic => ({                             ...diagnostic,                             messageText: `Привет, Хабр! ${diagnostic.messageText}`,                         }));                     };                 }                  return target[property];             },         }), });  export = plugin; 

По итогу получаем следующий результат:

Здесь и далее я использую Proxy для модификации оригинального languageService. Однако можно воспользоваться и «классическим» копированием объекта. Оно описано в официальном гайде.

Разбор основных параметров

Разберём подробнее приведённый выше код, чтобы узнать, какие функции нам доступны:

  • ts.server.PluginModuleFactory — функция, аргумент которой содержит текущий экземпляр typescript. Он нам понадобится в дальнейшем. Возвращать функция должна объект со следующими ключами:

  • getExternalFiles— необязательная функция. Она возвращает список файлов, которые плагин хочет рассматривать в процессе работы. Они могут и не быть частью проекта TypeScript. Например, если мы пишем плагин для vue, все файлы vue должны быть включены в проект;

  • onConfigurationChanged — необязательная функция. Вызывается, когда изменяется конфигурация плагина. Когда TypeScript сервер получает новую конфигурацию через файл tsconfig.json, эта функция может реагировать на изменения настроек и применять их к работе плагина;

  • create — обязательная функция и самая важная для разработчика плагина. Её аргумент — объект PluginCreateInfo. Его мы разберём позже. Функция должна вернуть LanguageService— это основной объект, с которым мы будем работать. Он предоставляет основные инструменты для работы с кодом: автодополнение, навигацию по коду (go to definition), показ ошибок, показ типов и документацию;

  • PluginCreateInfo содержит следующие ключи:

  • project — предоставляет доступ к данным текущего проекта, включая список файлов, информацию о зависимостях и другие проектные метаданные. Плагин может использовать эту информацию, чтобы адаптироваться к структуре проекта и выполнять операции, требующие понимания проектной организации;

  • languageService — оригинальный LanguageService был описан выше;

  • languageServiceHost — предоставляет информацию, необходимую для работы languageService. С его помощью плагин может запрашивать файлы, настройки компиляции, а также доступ к синтаксическим и семантическим данным проекта;

  • serverHost — предоставляет доступ к низкоуровневым операциям с файловой системой и проектом. Плагин может использовать его для чтения файлов, управления директориями и выполнения других операций с файловой системой, которые требуются для работы плагина;

  • session — это ключ, который предоставляет доступ к сессии сервера TypeScript. Сессия отслеживает текущее состояние работы редактора с проектом, управляет запросами между IDE и сервером TypeScript. Она позволяет плагину взаимодействовать с редактором и использовать такие функции, как регистрация команд или расширение возможностей обработки запросов;

  • config — содержит пользовательские настройки плагина, которые могут быть переданы через tsconfig.json. Эти параметры позволяют гибко настроить поведение плагина в зависимости от потребностей конкретного проекта.

Начинаем писать плагин для переопределения tsconfig.json

Из всего многообразия доступных параметров нам нужны: typescript из аргумента функции плагина, languageService, languageServiceHost и config из функции create.

Основная наша задача — создать отдельные languageService со своими настройками для каждой переопределённой папки.

Начнём с простого — определим настройки для нашего плагина. Это будет простой интерфейс:

import type ts from 'typescript/lib/tsserverlibrary';  export interface Override {     files: string[];     compilerOptions: ts.server.protocol.CompilerOptions; }  export interface PluginConfig {     overrides: Override[]; } 

ts.server.protocol.CompilerOptions — это любые настройки, доступные в tsconfig.json

Передаём эти настройки в tsconfig.json:

{   "compilerOptions": {     "strict": false,     "plugins": [       {         "name": "ts-overrides-plugin",         "overrides": [           {             "files": ["src/modern/**/*.{ts,tsx}"],             "compilerOptions": {               "strict": true,             },           },         ]       },     ]   }, } 

Для определения путей я решил использовать glob-паттерн. Для разработчиков он более привычен как по самому tsconfig (поля files, includes, exclude, etc), так и по eslint.config.

Теперь мы можем получить эти настройки внутри плагина:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({     create: info => {         const { overrides } = info.config as PluginConfig;          console.log(overrides);          return info.languageService;     }, });  export = plugin; 

Далее нам нужно создать отдельный languageService для каждой папки, требующей переопределения настроек TypeScript. Создадим функцию getOverrideLanguageServices и подробно её разберём:

import outmatch from 'outmatch';  const getOverrideLanguageServices = (     typescript: typeof ts,     overrides: Override[],     languageServiceHost: ts.LanguageServiceHost, ): ts.LanguageService[] =>     [...overrides].reverse().map(override => {         const overrideLanguageServiceHost: ts.LanguageServiceHost = {             fileExists: path => !!languageServiceHost.fileExists?.(path),             getCurrentDirectory: (): string => languageServiceHost.getCurrentDirectory(),             getDefaultLibFileName: (options: ts.CompilerOptions): string =>                 languageServiceHost.getDefaultLibFileName(options),             getScriptSnapshot: fileName => languageServiceHost.getScriptSnapshot(fileName),             getScriptVersion: fileName => languageServiceHost.getScriptVersion(fileName),             readFile: (path, encoding) => languageServiceHost.readFile?.(path, encoding),             getCompilationSettings: () => ({                 ...languageServiceHost.getCompilationSettings(),                 ...typescript.convertCompilerOptionsFromJson(                     override.compilerOptions,                     languageServiceHost.getCurrentDirectory(),                 ).options,             }),             getScriptFileNames: () => {                 const originalFiles = languageServiceHost.getScriptFileNames();                 const isMatch = outmatch(override.files);                  return originalFiles.filter(fileName =>                     isMatch(relative(languageServiceHost.getCurrentDirectory(), fileName)),                 );             },         };          return typescript.createLanguageService(overrideLanguageServiceHost);     }); 

Функция принимает параметры:

  • typescript. Он нужен нам для создания экземпляра languageService;

  • overrides — массив наших переопределений. Для каждого мы создадим отдельный languageService;

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

Для начала сделаем reverse массива overrides. Это нужно для того, чтобы при поиске через find мы получали последний languageService, способный обработать файл на наличие ошибок (по аналогии с eslint).

Далее для каждого override создадим languageServiceHost, определив обязательные методы:

  • fileExists должен вернуть при вызове true, если файл существует. Здесь, как и много где дальше, мы можем просто вызвать метод из оригинального languageServiceHost. Стоит учесть и то, что в typescript ≤ 4 этот метод был необязательным. Поэтому при вызове используем optional chaining;

  • getCurrentDirectory получает текущую директорию проекта (корневую папку). Используем оригинальный метод;

  • getDefaultLibFileName возвращает имя файла библиотеки по умолчанию (обычно lib.d.ts). Используется для определения файла стандартной библиотеки TypeScript, который подключается к проекту. Используем оригинальный метод;

  • getScriptSnapshot возвращает снимок текущего состояния файла. Снимки используются TypeScript для анализа изменений в коде без необходимости перечитывать файл каждый раз. Используем оригинальный метод;

  • getScriptVersion возвращает текущую версию скрипта (файла), по которой TypeScript может определить, изменился ли файл. Эта строка позволяет TypeScript определять, изменился ли файл, и нужно ли его анализировать повторно;

  • readFile читает содержимое файла по указанному пути path с возможностью указать кодировку файла (encoding). Как и в случае с fileExists, здесь используется безопасный вызов с проверкой на наличие метода, так как в typescript ≤ 4 этот метод был необязательным. Используем оригинальный метод;

  • getCompilationSettings возвращает объект настроек компиляции TypeScript (ts.CompilerOptions). Этот метод объединяет настройки, которые предоставляет оригинальный languageServiceHost, с нашими настройками компиляции из объекта override.compilerOptions. По сути, здесь происходит основная «магия» нашего плагина. Метод использует функцию convertCompilerOptionsFromJson, которая преобразует JSON-объект с настройками в валидные параметры компилятора TypeScript. Например, поле module, которое можно задать разными способами (CommonJS, commonjs), он преобразует в число;

  • getScriptFileNames возвращает массив имён всех файлов скриптов в проекте. Сначала вызывается оригинальный метод getScriptFileNames из languageServiceHost, который возвращает список всех файлов, известных TypeScript. Затем с помощью функции outmatch проверяется, какие из этих файлов соответствуют паттернам, указанным в override.files. Фильтрация позволяет плагину работать только с файлами, которые соответствуют этим паттернам. Вместо outmatch можно было использовать любую другую библиотеку для работы с glob-паттернами.

На основе созданного объекта overrideLanguageServiceHost, мы создаём languageService с помощью typescript.createLanguageService(overrideLanguageServiceHost). Далее именно его мы будем вызывать для файлов, в которых нужно переопределить настройки.

Определим функцию для получения languageService по имени файла:

const getLanguageServiceForFile = (     fileName: string,     overrideLanguageServices: ts.LanguageService[],     originalLanguageService: ts.LanguageService, ): ts.LanguageService => {     const overrideServiceForFile = overrideLanguageServices.find(         override => override.getProgram()?.getRootFileNames().includes(fileName),     );      if (overrideServiceForFile) {         return overrideServiceForFile;     }      return originalLanguageService; }; 

Здесь мы проходимся по массиву созданных выше languageService и ищем тот, что готов проанализировать файл. Если не находим, берём оригинальный. На этом почти всё. Нам осталось только определить основную функцию плагина:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({     create: info => {         const { overrides } = info.config as IdePluginConfig;          const overrideLanguageServices = getOverrideLanguageServices(typescript, overrides, info.languageServiceHost);          return new Proxy(info.languageService, {             get: (target, property: keyof ts.LanguageService) => {                 if (property === `getSemanticDiagnostics`) {                     return (fileName: string) => {                         const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target);                          return overrideForFile.getSemanticDiagnostics(fileName);                     };                 }                  return target[property];             },         });     }, });   export = plugin; 

Здесь мы переопределяем оригинальный метод getSemanticDiagnostics, который возвращает диагностику по файлу. В нём мы получаем переопределённый languageService, созданный нами, и проводим с его помощью диагностику. По итогу в одном проекте мы видим разный вывод ошибок для разных папок:

Устраняем проблемы

Плагин выполняет свою основную задачу — с его помощью мы получаем разную диагностику для разных файлов. Он был написан на коленке, а потому с ним возникли некоторые проблемы. Разберём их подробнее.

Память

Взглянув на код, можно определить источник теоретических проблем — повышенное потребление памяти. Мы создаём несколько languageServiceHost и languageService, каждый из которых хранит состояние проекта: информацию о типах, содержимое файлов и т.д. На большом проекте и с большим количеством overrides потребление памяти может сильно возрасти.

Как сгладить ситуацию? Например, с помощью typescript.createDocumentRegistry(). Он возвращает DocumentRegistry, который может хранить состояние файлов, их версии, выгружать неиспользуемые, а самое главное — шарить это состояние между разными экземплярами languageService. К сожалению, мы не можем получить DocumentRegistry из оригинальногоlanguageService, поэтому создадим новый и будем использовать его для всех наших languageService:

const getOverrideLanguageServices = (     typescript: typeof ts,     overridesFromConfig: Override[],     languageServiceHost: ts.LanguageServiceHost,     docRegistry: ts.DocumentRegistry, ): ts.LanguageService[] =>     [...overridesFromConfig].reverse().map(override => {         // ...         return typescript.createLanguageService(overrideLanguageServiceHost, docRegistry);     });  const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({     create: info => {         // ...         const docRegistry = typescript.createDocumentRegistry();          const overrideLanguageServices = getOverrideLanguageServices(             typescript,             overrides,             info.languageServiceHost,             docRegistry,         );          const originalLanguageServiceWithDocRegistry = typescript.createLanguageService(             info.languageServiceHost,             docRegistry,         );         //...     }, }); 

Таким образом, мы сильно оптимизируем работу с памятью.

Глобальные типы

При тестировании на реальном проекте вскрылась проблема — TypeScript начал ругаться на отсутствие глобальных типов, описанных в d.ts файлах. Ошибка была в создании LanguageServiceHost, в формировании массива файлов:

getScriptFileNames: () => {     const originalFiles = project.getScriptFileNames();     const isMatch = outmatch(override.files);      return originalFiles.filter(fileName => isMatch(relative(project.getCurrentDirectory(), fileName))); }, 

Здесь мы не учитываем d.ts файлы, если они явно не указаны в override.files. Проблема решается их явным добавлением в массив:

getScriptFileNames: () => {     const originalFiles = project.getScriptFileNames();     const isMatch = outmatch(override.files);      return originalFiles.filter(         fileName =>             fileName.endsWith(`.d.ts`) || isMatch(relative(project.getCurrentDirectory(), fileName)),     ); } 

Подсветка типов

Мы переопределили только метод getSemanticDiagnostics, который выводит ошибки. Но забыли про подсказки о типах. Например, создадим файл legacy.ts, в котором strict выключен:

Из-за отсутствия strictNullChecks, итоговый тип переменной из string | undefined превращается в string. Импортируем эту переменную из другого файла modern.ts, где strict включён:

Ошибка отображается верно, но тип переменной всё ещё остаётся string. Чтобы это исправить, переопределим ещё один метод в languageService:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({     create: info => {         // ...          return new Proxy(originalLanguageServiceWithDocRegistry, {             get: (target, property: keyof ts.LanguageService) => {                 if (property === `getQuickInfoAtPosition`) {                     return ((fileName: string, position: number) => {                         const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target);                          return overrideForFile.getQuickInfoAtPosition(fileName, position);                     });                 }                  // ...                  return target[property as keyof ts.LanguageService];             },         });     }, }); 

Теперь и ошибка, и тип отображается корректно:

Дебаггинг

Поскольку плагин работает в IDE, обычный console.log или точка остановы не помогут нам в его отладке. Можно использовать 2 решения для отладки:

  • писать логи в файл средствами nodejs;

  • использовать info.project.projectService.logger.info, запустить VSCode или WebStorm в режиме отладки и читать логи также из файла.

Совместимость с разными версиями TypeScript

Изначально я писал плагин только под версию TypeScript 5.3 и использовал Proxy для создания не только languageService, но и languageServiceHost — всё работало хорошо. В обновлении TypeScript 5.4 разработчики изменили поведение languageServiceHostи при использовании плагина любые ошибки типов перестали выводиться. Почитав исходники TypeScript, я нашёл участок кода, который косвенно запрещает использование одного экземпляра languageServiceHostнесколькими languageService. Это сделано с целью оптимизации. Поэтому я переписал Proxy на создание нового объекта с нуля.

Как было описано ранее, нужно быть осторожным с более старыми версиями TypeScript — некоторые методы там отсутствовали или были опциональными.

В целом плагин, который вмешивается в стандартное поведение TypeScript, достаточно шаткая история. Каждое изменение версии TypeScript требует отладки.

Неправильное понимание Program

Изначально я пытался создать ts.Program с помощью typescript.createProgram(), передав в него переопределённые настройки, а не создавать новые languageService. Далее в методе getSemanticDiagnosticsя пытался вызывать методы из созданной ts.Program. Что-то вроде этого:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({     create: info => {         // ...          const newProgram = typescript.createProgram([`modern.ts`], {             ...defaultOptions,             ...overrideOptions,         });          return new Proxy(info.languageService, {             get: (target, property: keyof ts.LanguageService) => {                 if (property === `getSemanticDiagnostics`) {                     return ((fileName: string) => {                         // ...                         const sourceFile = newProgram.getSourceFile(fileName);                          return newProgram.getSemanticDiagnostics(sourceFile);                     });                 }                  return target[property];             },         });     }, }); 

Но этот способ переставал работать при первом же изменении файла, расположение диагностики уезжало:

https://lh7-rt.googleusercontent.com/slidesz/AGV_vUf57SJodPZu6IW5DM_yDjBlN6RKPo0JMJUycxyvurtrQbrEVXdcmzCc4_7ahI4Gmc4eMzIiO69_yMI_7ePY2JG6E2jph4FR1xGhYvmV67I_ovYH8ZZuDHSJtdtdR49HHstBAd_yUa_wYOFp6IeYn5_1SqEgMlM=s2048?key=ZBKvTn_p51lwBzt4p8OcNg

Дело в том, что typescript.createProgram() создаёт «слепок» всего проекта на момент создания. Он не предназначен для использования в реальном времени с постоянно меняющимися файлами. Можно бы было создавать новый ts.Program при каждом вызове getSemanticDiagnosticsили при изменении файлов, но такой способ оказался очень ресурсозатратным и не нативным.

Бонус. Игнорирование файлов из проверки типов

Существуют редкие ситуации, когда нам нужно исключить файл из проверки типов. Например, это может быть JS-код, который нужно переписать на TypeScript. Если файл один — это не проблема. Но если это целая папка, в некоторых файлах захочется временно отключить проверку типов.

TypeScript позволяет заглушать ошибки для конкретных строк (// @ts-ignore) и для всего файла (// ts-nocheck). Но такие комментарии будут разбросаны по файлам — их легко потерять. С помощью плагина можно держать такие исключения в одном месте, в tsconfig.json:

"compilerOptions": {     "plugins": [       {         "name": "ts-overrides-plugin",         "ignores": ["src/ignored/**/*.{ts,tsx}"]     ]   } 

Что касается реализации — к вышеупомянутому коду добавляется пара строк:

const { overrides, ignores } = info.config as IdePluginConfig; const ignoresMatcher = ignores ? outmatch(ignores) : null;  // ...  if (property === `getSemanticDiagnostics`) {     return (fileName => {         if (ignoresMatcher?.(relative(info.project.getCurrentDirectory(), fileName))) {             return [];         }          const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target);          return overrideForFile.getSemanticDiagnostics(fileName);     }) as ts.LanguageService['getSemanticDiagnostics']; } 

Что же дальше?

А дальше мы запускаем команду tsc и видим, что наш плагин не работает. И это ожидаемо — плагин предназначен только для IDE.

Мы уже можем отлавливать новые ошибки на этапе разработки, но на этапе транспиляции они всё ещё могут проскользнуть. Нас это не устраивает. А написание плагина для транспиляции — это совершенно другой мир со своими тонкостями и костылями. О нём я расскажу в следующей статье.

P.S. А с кодом плагина можно можно ознакомиться по ссылке.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Используете ли вы strict в своих TypeScript проектах?

73.68% Да14
21.05% Нет4
5.26% Нет, но собираюсь переехать на него1

Проголосовали 19 пользователей. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/858308/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *