Типобезопасный словарь в машинописи - PullRequest
0 голосов
/ 13 апреля 2020

Возможно ли в Typescript дать мне возможность вернуть определенный тип в зависимости от аргумента WidgetType? Например - если я укажу WidgetType.chart, то widgetFactory будет присвоен тип [ChartView, ChartModel].

    enum WidgetType {
        chart = 0,
        table = 1,
        // etc...
    }

    class ViewBase {}
    class ModelBase {}

    class ChartView extends ViewBase {}
    class ChartModel extends ModelBase {}
    class TableView extends ViewBase {}
    class TableModel extends ModelBase {}


    class Registry<T1 extends ViewBase = ViewBase,T2 extends ModelBase = ModelBase> {
        private dict:{[key in WidgetType]?:[new() => T1,new() => T2]} = {};

        public addWidget(type:WidgetType,view:new() =>T1,model:new() => T2){
            this.dict[type] = [view,model];
        }

        public getWidget(type:WidgetType){
            return this.dict[type];
        }
    }


    const registry = new Registry();


    // Init app ...
    registry.addWidget(WidgetType.chart,ChartView,ChartModel);
    registry.addWidget(WidgetType.table,ChartView,ChartModel);
    //

    const widgetFactory = registry.getWidget(WidgetType.chart);
    if(widgetFactory){
        const view = new widgetFactory[0]();
        const model = new widgetFactory[1]();
    }

1 Ответ

1 голос
/ 13 апреля 2020

Прежде всего, система типов TypeScript структурная , а не номинальная, поэтому, если вы хотите, чтобы компилятор надежно различал guish между двумя типами, они должны иметь различную структуру. В примере кода люди обычно пишут пустые типы, такие как class Foo {} и class Bar {}, но компилятор видит их как тот же тип , несмотря на отличающиеся имена, Решение состоит в том, чтобы добавить фиктивное свойство для каждого типа, чтобы сделать их несовместимыми. Так что я просто собираюсь сделать это:

class ViewBase { a = 1 }
class ModelBase { b = 2 }
class ChartView extends ViewBase { c = 3 }
class ChartModel extends ModelBase { d = 4 }
class TableView extends ViewBase { e = 5 }
class TableModel extends ModelBase { f = 6 }

, чтобы избежать любых возможных проблем здесь.


Для версий TypeScript до 3.7 мое предложение будет:

  • создайте свой реестр одновременно, например, с помощью конструктора, который требует, чтобы вы поместили в него все в начале, например:

    const registry = new Registry({
      WidgetType.chart: [ChartView, ChartModel], 
      WidgetType:table: [TableView, TableModel]
    });
    

    или:

  • имеет метод addWidget(), возвращающий новый объект реестра, и требует, чтобы вы использовали метод цепочки вместо повторного использования исходного объекта реестра, например:

    const registry: Registry = new Registry()
      .addWidget(WidgetType.chart, ChartView, ChartModel)
      .addWidget(WidgetType.table, TableView, TableModel);
    

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

Но вы хотите видеть, что каждый раз, когда вы вызываете addWidget() для Registry объекта, компилятор изменяет тип объекта, чтобы учесть тот факт, что виджет определенного типа имеет конкретную модель и тип представления, связанный с ним. TypeScript не поддерживает произвольное изменение типа существующего значения. Он поддерживает временное сужение типа значения с помощью анализа потока управления , но до TypeScript 3.7 не было никакого способа сказать, что такой метод, как addWidget() должно привести к такому сужению.


В TypeScript 3.7 введены функции утверждения , которые позволяют вам сказать, что функция, возвращающая пустоту, должна запускать заданное c сужение потока управления один из аргументов функции или объект, для которого вы вызываете метод (если функция является методом). Синтаксис для этого должен иметь тип возврата метода, похожий на asserts this is ....

Вот один способ дать наборы для Registry, чтобы использовать подпись подтверждения с addWidget():

class Registry<D extends Record<WidgetType & keyof D, { v: ViewBase, m: ModelBase }> = {}> {
    private dict = {} as { [K in keyof D]: [new () => D[K]["v"], new () => D[K]["m"]] };

    public addWidget<W extends WidgetType, V extends ViewBase, M extends ModelBase>(
        type: W, view: new () => V, model: new () => M):
        asserts this is Registry<D & { [K in W]: { v: V, m: M } }> {
        (this.dict as any as Record<W, [new () => V, new () => M]>)[type] = [view, model];
    }

    public getWidget<W extends keyof D>(type: W) {
        return this.dict[type];
    }
}

Здесь я сделал Registry generi c в одном параметр типа D, представляющий словарь, который отображает подмножество перечислений WidgetType на пару типов представлений и моделей. По умолчанию будет использоваться пустой тип объекта {}, поэтому значение типа Registry означает реестр, к которому еще ничего не добавлено.

Свойство _dict относится к отображенному типу , которое принимает D и присваивает своим свойствам конструкторы с нулевым аргументом в кортеже. Обратите внимание, что компилятор не может знать, что {} является допустимым значением этого типа, поскольку он не понимает, что при создании объекта Registry он всегда будет иметь D из {}, поэтому мы требуется утверждение типа.

Метод addWidget() является обобщенным методом утверждения c, который принимает перечисление типа W, конструктор представления типа V и конструктор модели типа M и сужает тип this, добавляя свойство к D ... изменяя D на D & { [K in W]: { v: V, m: M } }.

Наконец, метод getWidget() позволяет указать только тип перечисления, который является ключом D. Делая это, мы можем сделать свойства словаря обязательными, а не необязательными, и getWidget() никогда не вернет undefined. Вместо этого вам не разрешат звонить getWidget() с перечислением, которое еще не было добавлено.


Давайте посмотрим на это в действии. Сначала мы создаем новый Registry:

// need explicit annotation below
const registry: Registry = new Registry();

Вот одно из больших предостережений использования функций утверждений в качестве методов: они работают, только если объект имеет явно аннотированный тип. См. microsoft / TypeScript # 33622 для получения дополнительной информации. Если вы написали const registry = new Registry() без аннотации, вы получите ошибки на addWidget(). Это болевая точка , и я не знаю, станет ли когда-нибудь или станет лучше.

С этими неприятностями позади нас, давайте двигаться дальше:

registry.getWidget(WidgetType.chart); // error! 
registry.getWidget(WidgetType.table); // error! 

Это ошибки, потому что в реестре еще нет виджетов.

registry.addWidget(WidgetType.chart, ChartView, ChartModel);

registry.getWidget(WidgetType.chart); // okay
registry.getWidget(WidgetType.table); // error! registry doesn't have that

Теперь вы можете получить виджет графика, но не виджет таблицы.

registry.addWidget(WidgetType.table, TableView, TableModel);

registry.getWidget(WidgetType.chart); // okay
registry.getWidget(WidgetType.table); // okay

И теперь вы можете делать ставки на оба виджета. Каждый вызов addWidget() сужал тип registry. Если вы наведите курсор мыши на registry и посмотрите на краткую информацию от IntelliSense, вы увидите эволюцию ее типа с Registry<{}> до Registry<{0: {v: ChartView; m: ChartModel;};}> до Registry<{0: {v: ChartView; m: ChartModel;};} & {1: {v: TableView; m: TableModel;};}>

И теперь мы можем использовать getWidget() Способ получения строго типизированных видов и моделей:

const chartWidgetFactory = registry.getWidget(WidgetType.chart);
const chartView = new chartWidgetFactory[0](); // ChartView
const chartModel = new chartWidgetFactory[1](); // ChartModel

const tableWidgetFactory = registry.getWidget(WidgetType.table);
const tableView = new tableWidgetFactory[0](); // TableView
const tableModel = new tableWidgetFactory[1](); // TableModel

Ура!


Так что все работает. Тем не менее, методы утверждения fr agile. Вам нужно использовать явные аннотации в некоторых местах. И все сужения анализа потока управления временные и не сохраняются в замыканиях (см. microsoft / TypeScript # 9998 ):

function sadness() {
    registry.getWidget(WidgetType.chart); // error!
}

Компилятор не ' не знает и не может понять, что к моменту вызова sadness(), что registry будет полностью сконфигурировано. Таким образом, я все еще вероятно рекомендовал бы одно из оригинальных решений. Для полноты картины я покажу вам, как выглядит решение цепочки методов:

    public addWidget<W extends WidgetType, V extends ViewBase, M extends ModelBase>(
        type: W, view: new () => V, model: new () => M) {
        const thiz = this as any as Registry<D & { [K in W]: { v: V, m: M } }>;
        thiz.dict[type] = [view, model];
        return thiz;
    }

Разница: вместо asserts this is XXX мы просто возвращаем this, и утверждение типа возвращаемого значения имеет значение введите XXX. Тогда предыдущий код использования будет представлен как:

const registry0 = new Registry();

registry0.getWidget(WidgetType.chart); // error! 
registry0.getWidget(WidgetType.table); // error! 

const registry1 = registry0.addWidget(WidgetType.chart, ChartView, ChartModel);

registry1.getWidget(WidgetType.chart); // okay
registry1.getWidget(WidgetType.table); // error! registry doesn't have that

const registry = registry1.addWidget(WidgetType.table, TableView, TableModel);

registry.getWidget(WidgetType.chart); // okay
registry.getWidget(WidgetType.table); // okay

const chartWidgetFactory = registry.getWidget(WidgetType.chart);
const chartView = new chartWidgetFactory[0](); // ChartView
const chartModel = new chartWidgetFactory[1](); // ChartModel

const tableWidgetFactory = registry.getWidget(WidgetType.table);
const tableView = new tableWidgetFactory[0](); // TableView
const tableModel = new tableWidgetFactory[1](); // TableModel

function happiness() {
    registry.getWidget(WidgetType.chart); // okay
}

, где вы присваиваете имена registry0 и registry1 промежуточным объектам реестра. На практике вы, вероятно, будете использовать цепочку методов и никогда не будете давать имена промежуточным вещам. Обратите внимание, что функция sadness() теперь happiness(), потому что всегда известно, что registry полностью сконфигурирован и нет необходимости беспокоиться об анализе потока управления.


Хорошо, надеюсь, это поможет. Удачи!

Детская площадка ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...