У меня есть несколько классов, экземпляры которых мне нужно будет создать на основе строки. У каждого класса есть модель, которую необходимо передать в конструктор, и все они разные (некоторые схожие свойства, но мы оставим это простым).
Я пытаюсь создать метод, который допускает два параметра: один определяет, экземпляр какого класса должен быть создан; а второй параметр следует сузить до модели, необходимой для этого класса.
Например, если я передаю MenuElementEnum.Products
в качестве первого параметра, только тогда вам будет разрешено передать объект ProductModel
в качестве второго параметра.
enum MenuElement {
Categories = 'Categories',
Products = 'Products',
}
type ProductModel = {
name:string;
age:number
id:string;
}
type CategoryModel = {
name:string;
favorite:boolean
id:string;
}
class Product implements ProductModel{
name =''
age= 0;
id = '';
constructor(model:ProductModel)
{
}
}
class Category implements CategoryModel{
name=''
favorite=true
id=''
constructor(model:CategoryModel)
{
}
}
const MenuElements = {
[MenuElementEnum.Products]:Product,
[MenuElementEnum.Categories]:Category
}
type KeysInEnum = keyof typeof MenuElementEnum
type ModelTypeForNewElement<Z extends keyof typeof MenuElements, Type extends typeof MenuElements[Z]> = {
[Prop in keyof Type]: Type[Prop];
}
type GEN = keyof typeof MenuElementEnum;
class DynamicMenuElement {
static instance: DynamicMenuElement;
public static get() {
if (!DynamicMenuElement.instance) {
DynamicMenuElement.instance = new DynamicMenuElement();
}
return DynamicMenuElement.instance;
}
public createMenuElement<T extends GEN>(type: T, model: ModelTypeForNewElement<T,typeof MenuElements[T]>) {
const ElementType = MenuElements[type]
if (!ElementType) {
throw new Error(`Cannot find the class type of ${type}`);
}
return new ElementType(model);
}
}
const creator = DynamicMenuElement.get();
creator.createMenuElement(MenuElementEnum.Categories,/*code hinting says a Category [model] is required here (expected) */)
creator.createMenuElement(MenuElementEnum.Product,/*code hinting says a Product [model] is required here (expected) */)
Что здесь происходит... ElementType объединяет обе модели следующим образом:
const ElementType: new (model: ProductModel & CategoryModel) => Product | Category
Когда то, что я ожидал/хотел:
(model:ProductModel | CategoryModel) => Product | Category
Я пробовал самые разные подходы. Это самое близкое, что я подошел. Я не знаю, почему аргумент «модель» объявляется как все мои псевдонимы типов, сжатые вместе:
(model:ProductModel | CategoryModel) => Product | Category
и не:
(model:ProductModel & CategoryModel) => Product | Category
(Пожалуйста, посмотрите предыдущий комментарий и внесите соответствующие изменения.) TS не может моделировать корреляции так, как вы это делаете. Единственный поддерживаемый подход описан по адресу ms/TS#47109 . Я могу использовать ваши существующие MenuElements
и выражать типы так, как понимает TS, как показано в этой ссылке на игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?
Я все еще работаю над этим примером.. Я совсем не понимаю, как работают MenuElementsParam
и MenuElementsReturn
, поэтому сложно сказать.. Хотя это действительно желаемый результат.. Я думаю, что использование «нового» оператор, поскольку тип члена псевдонима типа меня немного сбивает с толку.. [K in keyof _MenuElements]: _MenuElements[K] extends *new* (arg: infer A) => any ? A : never
• Ваш инициализатор const MenuElementEnum
по-прежнему синтаксически недействителен; это должно быть const
(в этом случае вам нужно исправить инициализатор) или enum
(в этом случае вам следует написать это вместо этого)? • Вы хотите сказать, что не сможете определить, решает ли мой подход этот вопрос, пока я сначала не объясню, как именно он работает? Это похоже на ловушку-22, поскольку я не хочу писать полное объяснение чего-то, что оказывается не тем, что вы пытаетесь сделать. Я рад объяснить это, когда напишу реальный ответ. Что вы думаете?
да, извините, на самом деле это перечисление в моем коде. Я не уверен, почему я это изменил... Я считаю, что это действительно отвечает на мой вопрос, и просто понятия не имею, как работают те три типа, которые вы написали, ха-ха. Если бы вы могли написать подробный ответ, я был бы очень признателен. Я вроде как понимаю слово «вывод», но не в этом контексте; Я также не понимаю, что/как/почему вы использовали оператор «новый» в этих типах. Спасибо!
Я собираюсь написать ответ, но было бы очень полезно, если бы вы взяли свой пример кода, поместили его в IDE и убедились, что он демонстрирует то, что вы говорите. Вы изменили имя перечисления, но не ссылки на него, и теперь в ваших дженериках есть другие ошибки. Не могли бы вы отредактировать так, чтобы единственной проблемой была та, на которую вы ссылаетесь? Обратите внимание: я прошу вас использовать IDE, чтобы случайно не создать еще одну проблему или не найти существующую.
(см. предыдущий комментарий) ваш код также не работает ModelTypeForNewElement
, поскольку он, по-видимому, ожидает, что вы передадите его конструктору, например creator.createMenuElement(MenuElement.Categories, Category)
вместо, я полагаю, creator.createMenuElement(MenuElement.Categories, { favorite: true, id: "abc", name: "" })
. Это действительно необходимо исправить в вопросе, иначе он вообще не демонстрирует вашу проблему.
Проблема в том, что TypeScript не может посмотреть тип MenuElements
и увидеть, что для произвольного общего ключа type
вы можете написать new MenuElements[type](model)
где model
— ожидаемый ввод, соответствующий конструктору. Он не видит абстрактной корреляции между type
и model
. В итоге он расширяется MenuElements[type]
до своего ограничения , которое представляет собой объединение typeof Product | typeof Category
. А объединения функций/конструкторов требуют пересечений аргументов для безопасного вызова (см. примечания к выпуску TypeScript 3.3, описывающие эту функцию).
Неспособность TypeScript следовать коррелированным объединениям описана в microsoft/TypeScript#30581.
Рекомендуемый подход к решению этой проблемы описан на странице microsoft/TypeScript#47109.
Идея состоит в том, что вам нужно написать тип MenuElements
так, чтобы это была явно одна структура для каждого ключа. Вы делаете это, делая его сопоставленным типом . Итак, вам понадобится const MenuElements: { [K in ⋯]: ⋯ } =
. И тип свойства этого отображаемого типа должен быть сигнатурой конструкции, которая связывает некоторые конкретные входные данные конструктора, которые зависят от K
, с конкретным типом экземпляра, который также зависит от K
. Итак, это должно выглядеть так
const MenuElements: { [K in keyof MenuElementsParam]:
new (arg: MenuElementsParam[K]) => MenuElementsReturn[K]
} = {
[MenuElement.Products]: Product,
[MenuElement.Categories]: Category
};
type MenuElementsParam = {
[MenuElement.Products]: ProductModel;
[MenuElement.Categories]: CategoryModel;
}
type MenuElementsReturn = {
[MenuElement.Products]: Product;
[MenuElement.Categories]: Category;
}
И если вы это сделаете, вы можете использовать MenuElementsParam
вместо сложного ModelTypeForNewElement
типа позывного для createMenuElement
:
function createMenuElement<K extends keyof MenuElementsParam>(
type: K, model: MenuElementsParam[K]
) {
const ElementType = MenuElements[type]
if (!ElementType) {
throw new Error(`Cannot find the class type of ${type}`);
}
return new ElementType(model); // okay, works
}
И если вы проверите подпись вызова, вы увидите, что она возвращается MenuElementsReturn[K]
:
/* function createMenuElement<K extends keyof MenuElementsParam>(
type: K, model: MenuElementsParam[K]): MenuElementsReturn[K] */
Это дает вам поведение, которое вы ищете:
const cat = createMenuElement(MenuElement.Categories, { favorite: true, id: "", name: "" });
// ^? const cat: Category
const pro = createMenuElement(MenuElement.Products, { age: 123, id: "", name: "" });
// ^? const pro: Product
Это отвечает на вопрос, но если вы решите добавить еще одно свойство к MenuElements
, вам придется внести кучу шаблонных изменений в MenuElementsParam
и MenuElementsReturn
. Было бы лучше, если бы вы могли просто написать MenuElements
, а затем TypeScript вычислил MenuElementsParam
и MenuElementsReturn
на его основе.
И это действительно возможно, с оговоркой, что вам нужно будет инициализировать MenuElements
в другой переменной, чтобы мы могли позже аннотировать тип переменной MenuElements
. Итак, давайте оставим исходный MenuElements
, но переименуем его:
const _MenuElements = {
[MenuElement.Products]: Product,
[MenuElement.Categories]: Category
}
И давайте проверим его тип:
type _MenuElements = typeof _MenuElements;
Отсюда нам нужно вытащить параметры и возвращаемые типы конструкторов. Мы можем сделать это, используя типы утилит ConstructorParameters и InstanceType:
type MenuElementsParam =
{ [K in keyof _MenuElements]: ConstructorParameters<_MenuElements[K]>[0] }
type MenuElementsReturn =
{ [K in keyof _MenuElements]: InstanceType<_MenuElements[K]> }
(отметим, что ConstructorParameters
дает кортеж всех параметров, но поскольку у вас есть только один и вы хотите получить его, нам нужно индексировать этот кортеж с помощью 0
, чтобы получить первый параметр.)
Или мы можем записать их самостоятельно, используя вывод условного типа, именно так в любом случае определяются эти типы утилит:
type MenuElementsParam = { [K in keyof _MenuElements]:
_MenuElements[K] extends new (arg: infer A) => any ? A : never
}
type MenuElementsReturn = { [K in keyof _MenuElements]:
_MenuElements[K] extends new (arg: any) => infer R ? R : never
}
Это просто проверка _MenuElements[K]
на сигнатуру конструкции new (arg: any) => any
) и infer
кольцевание интересующего нас фрагмента. Оба определения приводят к типам, эквивалентным версиям, определенным вручную, но теперь, если _MenuElements
изменится, они будут меняться автоматически.
Наконец, мы можем аннотироватьMenuElements
как тот же сопоставленный тип, что и раньше, но теперь мы инициализируем его значением _MenuElements
:
const MenuElements: { [K in keyof _MenuElements]:
new (arg: MenuElementsParam[K]) => MenuElementsReturn[K]
} = _MenuElements;
И мы закончили. Функция createMenuElement()
проверяет типы как вызывающих, так и реализаций.
Детская площадка, ссылка на код
Вопрос по поводу вывода. когда вы говорите _MenuElements[K] extends new (arg: infer A) => any ? A : never
откуда такой вывод? это следует из _MenuElements[K]
? ака new Product/Category(HERE?)
Да. Пожалуйста, ознакомьтесь с документацией по выводу внутри условных типов.
• Скриншоты IDE, к сожалению, здесь неуместны. Пожалуйста, замените изображения кода/журналов/ошибок/подсказок текстовыми версиями. • Ваш инициализатор
const MenuElementEnum
синтаксически недействителен. Пожалуйста, отредактируйте , чтобы убедиться, что ваш код представляет собой минимально воспроизводимый пример.