Как я закрыл трехлетний issue в TypeScript



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

    1. Файлы в папке lib/ напрямую изменять нельзя. Там находятся last-known-good версии и они периодически обновляются на основе соответствующих файлов из папки src/lib/. Вот в них то и нужно вносить правки. В нашем случае это src/lib/dom.generated.d.ts и src/lib/webworker.generated.d.ts
    2. Практически все файлы в директории 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.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      +1
      Что думаете по поводу условного workaround решения как подход к устранению таких базовых ограничений

      1. Создаем файл lib_extends.dom.d.ts
      /** Первый вариант */
      interface CustomMessageEvent<T = any> extends MessageEvent {
          data: T;
      }
      interface Worker {
          addEventListener<T>(message: string, handler: (message: CustomMessageEvent<T>) => any, options?: boolean | AddEventListenerOptions): any;
      }
      
      /** Второй вариант */
      interface Worker {
          addEventListener<T = any>(message: string, handler: (message: Omit<MessageEvent, "data"> & {data: T}) => any, options?: boolean | AddEventListenerOptions): any;
      }
      

      2. Добавляем в tsconfig.json в секцию include строку
          "include": [
              "./путо до lib_extends.dom.d.ts",
          ]
      

      3. И далее в коде проекта используем
      const worker = new Worker(...);
      
      worker.addEventListener<string>("message", (message => {
          message.data // (property) data: string
      }))
      
        +2

        В моих проектах используется такой подход:


        // globals.d.ts
        
        interface TypedMessageEvent<T> extends MessageEvent {
            data: T;
        }
        
        interface TypedWorkerEventMap<T, E = T> extends AbstractWorkerEventMap {
            "message": TypedMessageEvent<T>;
            "messageerror": TypedMessageEvent<E>;
        }
        
        interface TypedWorker<Input, Output> extends Worker {
        
            postMessage(message: Input, transfer: Transferable[]): void;
        
            postMessage(message: Input, options?: PostMessageOptions): void;
        
            addEventListener<K extends keyof TypedWorkerEventMap<Output>>
            (type: K, listener: (this: TypedWorker<Input, Output>, ev: TypedWorkerEventMap<Output>[K]) => any, options?: boolean | AddEventListenerOptions): void;
        }

        Это позволяет описать как все входящие так и исходящие сообщения


        type MessageStart = 'start';
        type MessageStop = 'stop';
        type MessageProgress = 'progress'
        
        type MessageInput = MessageStart | MessageStop
        type MessageOutput = MessageProgress
        
        const w = new Worker() as TypedWorker<MessageInput, MessageOutput>
        
        w.addEventListener('message', m => {
          m.data // 'start' | 'stop'
        })
        
        w.postMessage('some') // Type "some" is not assignable to type "MessageProgress"
        +1

        Мне всегда было интересно, а в чем преимущество генерации кода из json перед непосредственно кодом как есть?

          0

          Не совсем понял ваш вопрос

            +8

            Почему просто не хранится файл src/lib/d.ts в котором можно просто напрямую поменять код? Зачем вносить изменения в JSON из которых код сгенерируется?
            Ну, то есть я понимаю, что раз так делают, в этом есть какое-то преимущество, вот мне и интересно какое.

              +7

              Если коротко — для автоматизеции и уменьшения ручной работы.


              Если чуть подробнее:


              1. Источником всех типов являются .widl файлы. Они автоматически вытягиваются из браузера Edge. То есть, все веб интерфейсы пишутся не вручную а берутся из того, что имплементировано в браузере. Вот, описание MessageEvent, например.
                Так можно быстро и легко отслеживать все изменения в спецификациях веб-платформы.
              2. Многие такие интерфейсы должны быть представлены в нескольких библиотеках. Чтобы не приходилось согласовывать описание одного и того же интерфейса в разных библиотеках он вынесен в отдельный файл, а уже оттуда записывается в нужные библиотеки.

              А в формате JSON просто описаны дополнительные инструкции о том, как конвертировать widl в ts

                0

                Немного становится страшно, что они вытягиваются из Edge. А не Edge (вместе с остальными браузерами) вытягивает их откуда-то из центрального места.

          +9

          Спасибо что потратили свое время и сделали Typescript типы немножко лучше для остальных пользователей!

            +1
            Вот это мне в typescript нравится. Я когда засылал простой багфикс, приняли в течении дня. А потом ещё и в релиз ноутс, упоминают всех, даже если коммит состоял из перестановки двух строк. С точки зрения открытости сообществу они очень крутые.

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

            Самое читаемое