Всё началось с моего желания описать структуру сообщений между web worker'ами. К сожалению, на тот момент встроенные возможности TypeScript этого не позволяли.
Я засучил рукава и решил это исправить.
Суть проблемы
Попробуйте написать простой воркер и повесить на него слушателя событий. Посмотрите, какие типы выведет компилятор для параметров функции обратного вызова:
new Worker().addEventListener('message', (message) => { message // MessageEvent message.data // any})
В поле
data
находятся именно те данные, что вы, автор
кода, отправляете. И именно тип этого поля хочется определять.Изучаем MessageEvent поближе
MessageEvent интерфейс, описывающий сообщения при коммуникации между вкладками, воркерами, сокетами, WebRTC каналами и т.д.
В экосистеме TypeScript этот интерфейс является частью
lib.dom.ts
и lib.webworker.d.ts
, и описан
следующим образом:
interface MessageEvent extends Event { readonly data: any; readonly lastEventId: string; readonly origin: string; readonly ports: ReadonlyArray<MessagePort>; readonly source: MessageEventSource | null;}
Опытные разработчики сразу увидят тут проблему. Этот интерфейс не Generic поле
data
описано строго
как any, и мы никак не можем на это повлиять из вне.Решив поискать информацию по этому поводу в репозитории TypeScript я быстро нашел issue в котором описана эта проблема. И даже больше автор предложил решение. Но за почти три года, оно так и не было имплементировано. Я засучил рукава, и решил сделать это.
Всего-то и нужно что привести интерфейс MessageEvent, в файлах lib/lib.dom.d.ts и lib/lib.webworker.d.ts к такому виду:
interface MessageEvent<T = any> extends Event { readonly data: T; readonly lastEventId: string; readonly origin: string; readonly ports: ReadonlyArray<MessagePort>; readonly source: MessageEventSource | null;}
Сделаем это.
Изучаем TypeScript Instructions for Contributing Code
В нем есть целый раздел посвященный изменениям в файлах lib.d.ts. Оттуда узнаём две вещи:
- Файлы в папке lib/ напрямую изменять нельзя. Там находятся last-known-good версии и они периодически обновляются на основе соответствующих файлов из папки src/lib/. Вот в них то и нужно вносить правки. В нашем случае это src/lib/dom.generated.d.ts и src/lib/webworker.generated.d.ts
- Практически все файлы в директории src/lib/ можно просто отредактировать. За исключением генерируемых (.generated.d.ts). Такие файлы создаются с помощью утилиты TSJS-lib-generator и мы должны вносить правки именно в неё.
Изучаем TSJS-lib-generator
TSJS-lib-generator это инструмент (написанный на TS) который принимает все известные Microsoft Edge веб-интерфейсы и преобразовывает их в набор TypeScript интерфейсов. При этом существует возможность переопределить характеристики каких-либо интерфейсов, удалить некоторые или добавить новые.
Все эти правила описываются в json формате в файлах addedTypes.json, overridingTypes.json и removedTypes.json.
Правило для изменения MessageEvent
Нам нужно изменить существующий интерфейс, поэтому будем редактировать overridingTypes.json.
К сожалению, я не нашел подробной документации об синтаксисе этих файлов, но существующих примеров было достаточно для понимания концепции.
Итак, в overridingTypes.json в свойстве
interfaces
добавляем новый интерфейс, пока что без
каких либо свойств:
{ "interfaces": { "interface": { "MessageEvent": {} } }}
Пробуем запустить сборку и проверим, что ничего не сломалось:
npm run build
TSJS-lib-generator сгенерирует те самые *.generated.d.ts файлы. И сейчас они должны быть идентичны *.generated.d.ts файлам в репозитории TS.
Добавляем свойство
type-parameters
, тем самым
превращая MessageEvent в Generic:
{ "interfaces": { "interface": { "MessageEvent": { "type-parameters": [ { "name": "T", "default": "any" } ] } } }}
Запускаем сборку и проверяем результат:
interface MessageEvent<T = any> extends Event { readonly data: any; readonly lastEventId: string; readonly origin: string; readonly ports: ReadonlyArray<MessagePort>; readonly source: MessageEventSource | null;}
Уже ближе к тому, что мы в итоге хотим получить. Добавим описание свойства
data
и сигнатуры конструктора:
{ "interfaces": { "interface": { "MessageEvent": { "name": "MessageEvent", "type-parameters": [ { "name": "T", "default": "any" } ], "properties": { "property": { "data": { "name": "data", "read-only": 1, "override-type": "T" } } }, "constructor": { "override-signatures": [ "new<T>(type: string, eventInitDict?: MessageEventInit<T>): MessageEvent<T>" ] } } } }}
И вуаля! Генерируется в точности то, что нам необходимо.
interface MessageEvent<T = any> extends Event { readonly data: T; readonly lastEventId: string; readonly origin: string; readonly ports: ReadonlyArray<MessagePort>; readonly source: MessageEventSource | null;}
Далее дело за малым:
- Запустить тесты
- Оформить Pull Request в соответствии со всеми правилами описанными в Contribution Guidelines
- И подписать CLA от Microsoft.
Итог
Спустя неделю с момента отправки мой Pull Request был принят. 12.06.2020 были обновлены
src/lib/dom.generated.d.ts
и
src/lib/webworker.generated.d.ts
в репозитории
TypeScript. А на момент написания статьи все правки перенесены в
lib/lib.dom.ts
и
lib/lib.webworker.d.ts
и уже доступны в TypeScript Nightly.