Создайте динамический класс и второй тип параметра на основе первого

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

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

Например, если я передаю 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

• Скриншоты IDE, к сожалению, здесь неуместны. Пожалуйста, замените изображения кода/журналов/ошибок/подсказок текстовыми версиями. • Ваш инициализатор const MenuElementEnum синтаксически недействителен. Пожалуйста, отредактируйте , чтобы убедиться, что ваш код представляет собой минимально воспроизводимый пример.

jcalz 05.08.2024 19:13

(Пожалуйста, посмотрите предыдущий комментарий и внесите соответствующие изменения.) TS не может моделировать корреляции так, как вы это делаете. Единственный поддерживаемый подход описан по адресу ms/TS#47109 . Я могу использовать ваши существующие MenuElements и выражать типы так, как понимает TS, как показано в этой ссылке на игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?

jcalz 05.08.2024 19:21

Я все еще работаю над этим примером.. Я совсем не понимаю, как работают MenuElementsParam и MenuElementsReturn, поэтому сложно сказать.. Хотя это действительно желаемый результат.. Я думаю, что использование «нового» оператор, поскольку тип члена псевдонима типа меня немного сбивает с толку.. [K in keyof _MenuElements]: _MenuElements[K] extends *new* (arg: infer A) => any ? A : never

VarJohn 05.08.2024 22:24

• Ваш инициализатор const MenuElementEnum по-прежнему синтаксически недействителен; это должно быть const (в этом случае вам нужно исправить инициализатор) или enum (в этом случае вам следует написать это вместо этого)? • Вы хотите сказать, что не сможете определить, решает ли мой подход этот вопрос, пока я сначала не объясню, как именно он работает? Это похоже на ловушку-22, поскольку я не хочу писать полное объяснение чего-то, что оказывается не тем, что вы пытаетесь сделать. Я рад объяснить это, когда напишу реальный ответ. Что вы думаете?

jcalz 05.08.2024 22:28

да, извините, на самом деле это перечисление в моем коде. Я не уверен, почему я это изменил... Я считаю, что это действительно отвечает на мой вопрос, и просто понятия не имею, как работают те три типа, которые вы написали, ха-ха. Если бы вы могли написать подробный ответ, я был бы очень признателен. Я вроде как понимаю слово «вывод», но не в этом контексте; Я также не понимаю, что/как/почему вы использовали оператор «новый» в этих типах. Спасибо!

VarJohn 06.08.2024 15:04

Я собираюсь написать ответ, но было бы очень полезно, если бы вы взяли свой пример кода, поместили его в IDE и убедились, что он демонстрирует то, что вы говорите. Вы изменили имя перечисления, но не ссылки на него, и теперь в ваших дженериках есть другие ошибки. Не могли бы вы отредактировать так, чтобы единственной проблемой была та, на которую вы ссылаетесь? Обратите внимание: я прошу вас использовать IDE, чтобы случайно не создать еще одну проблему или не найти существующую.

jcalz 06.08.2024 15:14

(см. предыдущий комментарий) ваш код также не работает ModelTypeForNewElement, поскольку он, по-видимому, ожидает, что вы передадите его конструктору, например creator.createMenuElement(MenuElement.Categories, Category) вместо, я полагаю, creator.createMenuElement(MenuElement.Categories, { favorite: true, id: "abc", name: "" }). Это действительно необходимо исправить в вопросе, иначе он вообще не демонстрирует вашу проблему.

jcalz 06.08.2024 15:58
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
7
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема в том, что 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?)

VarJohn 06.08.2024 21:39

Да. Пожалуйста, ознакомьтесь с документацией по выводу внутри условных типов.

jcalz 07.08.2024 01:08

Другие вопросы по теме

Аргумент типа «Число» нельзя назначить параметру типа «строка | номер | ноль | неопределенный'. [плагин angular-компилятора]
Переход с Angular JS на Angular 18
Как сделать цвет фона прозрачным с помощью Phaser 3
TypeScript: [Страна, Штат], где параметры автозаполнения штата меняются в зависимости от введенной страны
После недопустимого входа на сервер ASP.NET MVC, как я могу записать текст ошибки в возвращенном ответе Json и отобразить его в клиенте Angular?
Angular – почему мой раскрывающийся список не работает
Angular V18 PM [vite] Внутренняя ошибка сервера: страница /home не отображается в течение 30 секунд
Как выполнить проверку Type Guard для индексированного свойства
Angular 18 @для возврата ОШИБКИ TypeError: newCollection[Symbol.iterator] не является функцией
Сохранение информации о типе при обеспечении связи типов между членами кортежа при использовании в параметре функции сопоставленного типа