Прежде всего, система типов 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
полностью сконфигурирован и нет необходимости беспокоиться об анализе потока управления.
Хорошо, надеюсь, это поможет. Удачи!
Детская площадка ссылка на код