Вступление

В своем первом сообщении в блоге я хотел бы остановиться на проблеме, которую я недавно решил с помощью API компилятора TypeScript. Я уверен, что я бы не смог добиться, чтобы что-то работало без помощи различных блогов и ответов StackOverflow, поэтому было довольно эгоистично не писать о своих знаниях с помощью мощного, но мало документированного набора инструментов.

Затронутые темы

Основы API компилятора TypeScript (терминология парсера, API преобразователя, многоуровневая архитектура), AST, шаблон посетителя, генерация кода.

Предварительное условие

@ Vaidehi Joshi имеет отличную статью об AST, которую я бы посоветовал прочитать, если вы не знакомы с концепцией. Ее серия basecs прекрасна, и вам стоит ее проверить.

Проблема, которую я решаю

Мы используем GraphQL в Avero и хотели добавить некоторую безопасность типов для резолверов. Я наткнулся на graphqlgen, который решил многие из этих проблем, с его концепцией моделей. Я не хочу слишком глубоко погружаться в эту тему в этом сообщении в блоге, но я надеюсь написать что-нибудь в будущем, что погрузится в GQL. tldr заключается в том, что модели представляют возвращаемое значение ваших преобразователей запросов (которое может отличаться от вашей схемы GQL), а в graphqlgen вы связываете эти модели с интерфейсами, используя какую-то конфигурацию (файл YAML или TypeScript с объявления типов).

На работе мы запускаем микросервисы gRPC, и GQL в основном служит хорошим слоем разветвления для наших потребителей пользовательского интерфейса. Мы уже публикуем интерфейсы TypeScript, которые соответствуют нашим прото контрактам, и я хотел использовать эти типы в graphqlgen в качестве наших моделей, но столкнулся с некоторыми проблемами из-за поддержки экспорта типов и со способом опубликованы наши интерфейсы TypeScript (много пространств имен, много ссылок).

Как и любой хороший сторонник открытого исходного кода, мой первый подход заключался в том, чтобы усилить работу, уже проделанную в репозитории graphqlgen, и попытаться внести значимый вклад. Для самоанализа своего типа graphqlgen использует @ babel / parser для чтения файла TypeScript (в моем случае) и сбора информации об именах интерфейсов и объявлениях (полях интерфейса).

Каждый раз, когда я хочу что-нибудь сделать с AST, я сразу же открываю astexplorer.net и начинаю экспериментировать. Этот инструмент позволяет нам исследовать AST, генерируемые множеством различных синтаксических анализаторов, включая как @ babel / parser, так и синтаксический анализатор компилятора TypeScript. Это дает нам отличный способ визуализировать структуры данных, с которыми мы будем работать, и ознакомиться с типами узлов AST для данного анализатора.

Давайте посмотрим на пример входного файла и соответствующий AST с помощью babel-parser:

Корень нашего AST (тип узла Program) имеет в своем теле два оператора: ImportDeclaration и ExportNamedDeclaration.

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

Точно так же в нашем ExportNamedDeclaration нам дается основная информация об исходном тексте. Пространства имен усложняют эту структуру, и она может быть произвольно вложенной, что добавляет все больше и больше QualifiedTypeIdentifiers. Это будет еще одна неловкая ситуация, которую нам нужно будет решить, если мы продолжим идти по этому пути.

Я еще даже не дошел до разрешения типов из импорта! Учитывая, что синтаксический анализатор и AST (по замыслу) ограничены информацией в исходном тексте, нам нужно будет проанализировать любые импортированные файлы, чтобы эта информация была доступна в нашем окончательном AST. Но этот импорт мог иметь свой собственный импорт!

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

Мы не хотим заниматься импортом, мы не хотим заботиться о файловой структуре. Мы хотим иметь возможность разрешать все свойства protos.user.User и встраивать их вместо того, чтобы полагаться на импорт. Как мы можем получить информацию об этом типе, чтобы начать сборку этого файла?

Введение в TypeChecker в TypeScript

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

Здесь сразу выделяется одна часть:

Из экземпляра программы можно создать TypeChecker. TypeChecker - это ядро ​​системы типов TypeScript. Это часть, отвечающая за выяснение отношений между символами из разных файлов, присвоение типов символам и генерацию любой семантической диагностики (т. Е. Ошибок).

Первое, что сделает TypeChecker, - это объединит все символы из разных исходных файлов в единое представление и построит единую таблицу символов, «объединив» любые общие символы (например, пространства имен, охватывающие несколько файлов).

После инициализации исходного состояния TypeChecker готов ответить на любые вопросы по программе. Такими «вопросами» могут быть:

Какой символ у этого узла?

Какой тип этого символа?

Какие символы видны в этой части AST?

Какие есть подписи для объявления функции?

О каких ошибках следует сообщать для файла?

TypeChecker звучит именно так, как нам нужно! Нам нужен доступ к базовой таблице символов и API, чтобы мы могли ответить на эти первые два вопроса: Что такое символ для этого узла? А что это за символ? Он даже упоминает о слиянии общих символов, поэтому он решает нашу проблему с пространством имен, о которой мы говорили ранее!

Таааак, как нам добраться до этого API?

Это - один из немногих примеров, которые я смог найти в Интернете, но этого достаточно, чтобы мы начали. Мы видим, что к проверке можно получить доступ из метода в нашем экземпляре Program. Глядя на использование в этом примере, мы можем увидеть такие методы, как checker.getSymbolAtLocation и checker.getTypeOfSymbolAtLocation, которые, по-видимому, являются, по крайней мере, некоторым вариантом того, что нам нужно.

Приступим к написанию нашей программы.

ts-node ./src/ts-alias.ts
prints
ImportDeclaration
TypeAliasDeclaration
EndOfFileToken

Здесь нас интересует объявление псевдонима типа, поэтому давайте обновим наш код, чтобы сосредоточиться на этом.

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

Теперь, когда у нас есть наш узел, мы хотим вернуться к ответам на два вопроса, которые мы упоминали ранее: Что такое символ для этого узла? И какой тип этого символа?

Итак, мы получили имена объявления интерфейса псевдонима типа, взаимодействуя с таблицей символов TypeChecker. Нам еще предстоит кое-что сделать, но это отличная отправная точка для самоанализа.

Давайте подумаем о поколении.

API трансформации

Мы показали нашу цель ранее ... учитывая файл TypeScript, проанализируем, проанализируем и создадим новый файл TypeScript. Сигнатура функции AST - ›AST невероятно распространена в программировании - достаточно, чтобы команда TypeScript выпустила API пользовательского преобразования , чтобы создать свой собственный!

Давайте напишем невероятно простой пользовательский преобразователь, прежде чем мы погрузимся в нашу первоначальную проблему. Спасибо Джеймсу Гарбутту за то, что он дал мне шаблон для начала.

Наш первый базовый преобразователь преобразует числовые литералы в строковые литералы.

Наиболее важные интерфейсы, о которых следует беспокоиться, - это Visitor и VisitorResult:

type Visitor = (node: Node) => VisitResult<Node>;
type VisitResult<T extends Node> = T | T[] | undefined;

Наша цель как автора нестандартного преобразователя - написать этого Посетителя. Мы рекурсивно посещаем каждый узел в нашем AST и возвращаем VisitResult, который может быть одним, многими или нулевыми узлами AST. Мы можем настроить таргетинг на определенные узлы, чтобы изменить их, оставив остальные в покое.

Здесь помечено AST, чтобы показать узлы, с которыми мы будем работать.

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

  1. Замените TypeAliasDeclarations на InterfaceDeclarations.
  2. Разрешить TypeReferences в TypeLiterals

Решение

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

Я был очень доволен тем, как получилось мое решение. Он демонстрирует мощь хороших абстракций, продуманного дизайна компилятора, отличных инструментов разработчика (автозаполнение VSCode, обозреватель AST и т. Д.) И небольшого количества аутсорсинга из опыта других умных людей. Полностью обновленный исходный код можно найти здесь. Я не уверен, насколько это будет полезно для всех, кто не является моим узким вариантом использования, но в основном я хотел продемонстрировать мощь инструментальной цепочки компилятора TypeScript, а также задокументировать свой мыслительный процесс по уникальной проблеме, которую я действительно не решал раньше.

Я надеюсь, что это будет полезно для всех, кто пытается делать подобные вещи. Если вас пугают такие темы, как AST, компиляторы и преобразования, я надеюсь, что дал вам достаточно шаблонов и ссылок на другие ресурсы, чтобы начать работу. Код здесь - это мой окончательный результат после длительного обучения. С закрытыми репозиториями Github мои первые попытки сделать это, включая все 45 утверждений // @ ts-ignores и ! в 150-строчном файле, могут скрываться в тени стыда.

Ресурсы, которые мне помогли