Создание типа обработчика динамических событий

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

class UIEvent { }
class MouseInput extends UIEvent { }
class KeyboardInput extends UIEvent { }

let Events = {
    MouseInput,
    KeyboardInput,
}

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

type EventTarget = {
    [K in keyof typeof Events]?:
      (event: InstanceType<typeof Events[K]>) => void;
}

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

Затем я пытаюсь использовать это в классе компонента, но не могу заставить компилятор принудительно применить интерфейс.

class Component implements EventTarget {
    // The compiler has no problems with this, even though it violates
    // the EventTarget interface...
    MouseInput(event: KeyboardInput) {

    }
}

Я сильно подозреваю, что здесь я пытаюсь работать против структурной типизации, потому что, как только я добавляю свойство, которое делает MouseEvent отличным от KeyboardEvent, проверки типов срабатывают.

class UIEvent<T> { t: T }
class MouseInput extends UIEvent<"MouseInput"> {}
class KeyboardInput extends UIEvent<"KeyboardInput"> {}

Это работает, но я бы предпочел использовать конструктор события в качестве дискриминатора, а не добавлять отдельный тип свойства.

И, наконец, я хочу распространить этот контракт на подтипы класса компонентов.

class Clickable extends Component {
    // This should also fail to typecheck, but interfaces aren't inherited
    MouseInput(event: KeyboardInput) {

    }
}

Самое близкое, что я могу сделать прямо сейчас, - это явно реализовать каждый подтип EventTarget.


person Dan Prince    schedule 09.11.2018    source источник
comment
Разве нельзя позволить Events = { MouseEvent, KeyboardEvent, } быть let Events = { MouseInput, KeyboardInput, }? Или let Events = { MouseEvent : MouseInput, KeyboardEvent: KeyboardInput, }?   -  person Titian Cernicova-Dragomir    schedule 09.11.2018
comment
Хорошее место. Фиксированный!   -  person Dan Prince    schedule 09.11.2018


Ответы (1)


Вы правы, вы здесь работаете против структурной типизации. Пока классы не имеют членов, чтобы различать их, компилятор будет считать их совместимыми.

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

export class UIEvent { }
class MouseInput extends UIEvent { public x!: number; public y!: number }
class KeyboardInput extends UIEvent { public key!: string }

let Events = {
    MouseInput,
    KeyboardInput,
}

type EventTarget = {
    [K in keyof typeof Events]?:
      (event: InstanceType<typeof Events[K]>) => void;
}

class Component implements EventTarget {
    MouseInput(event: KeyboardInput) { // Error now 

    }
}

Если вы не хотите раскрывать какой-либо член, вы можете просто объявить private член типа undefined. Это сделает типы несовместимыми, но не повлияет на время выполнения:

class MouseInput extends UIEvent { private isMouse: undefined }
class KeyboardInput extends UIEvent { private isKeyboard: undefined }
person Titian Cernicova-Dragomir    schedule 09.11.2018
comment
Спасибо, это хорошо. Есть идеи по распространению этого на подтипы? - person Dan Prince; 09.11.2018
comment
@DanPrince. Ваша идея использования универсального параметра работает, если вы хотите распространить его на базовый тип. Решение с частным полем работает только в том случае, если вы определяете такое поле в каждом классе. - person Titian Cernicova-Dragomir; 09.11.2018
comment
Извините, я имел в виду другое направление. Есть ли способ сделать так, чтобы тип Clickable захватил EventTarget интерфейс неявно через Component? - person Dan Prince; 09.11.2018