Как добавить ограничение на карту в общем c? - PullRequest
1 голос
/ 04 мая 2020

Редактировать : Я не достаточно ясно, вы можете перейти к следующему примеру.

У меня проблема с ограничениями на дженерики, и я не могу gr asp что я делаю не так.
По сути, я пытаюсь это:

enum Categories {
    FIRST = 'first',
    SECOND = 'second',
}

type ItemsMap = {
    [key in Categories]: Item<key>;
}

class Item<
    T extends keyof M,
    M extends {[key in T]: Item<key, M>} = ItemsMap,
> {
    category: T;
    items: M;
}

Цель состоит в том, чтобы передать «карту» перечисления / типа (Item позже потребуется использовать «тип» "), который, кажется, прекрасно работает, потому что VSCode показывает мне это:

type ItemsMap = {
    first: Item<Categories.FIRST, ItemsMap>;
    second: Item<Categories.SECOND, ItemsMap>;
}

Тем не менее, у меня есть ошибка TS по умолчанию M generi c:

Type 'ItemsMap' does not satisfy the constraint '{ [key in T]: Item<key, ItemsMap>; }'

Почему это не удовлетворяет ограничению?

У меня возникла другая проблема при попытке использовать эту карту в подклассе:

class Foo<M extends {[key in keyof M]: Item<key, M>}> {} // OK
class Bar<M extends ItemsMap = ItemsMap> extends Foo<M> {} // Not OK
class Baz extends Foo<ItemsMap> {} // OK, but why?

TS выдает ошибку на Bar:

Type 'M' does not satisfy the constraint '{ [key in keyof M]: Item<key, M>; }'.
    Type 'ItemsMap' is not assignable to type '{ [key in keyof M]: Item<key, M>; }'.

Но я не понимаю почему. Есть ли способ получить больше информации от TS?


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

interface EventInterface {
    target: EventTargetInterface;
    type: string;
}

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

interface EventTargetInterface {
    addEventListener(type: string, listener: (event: EventInterface) => void): void;
    dispatchEvent(event: EventInterface): boolean;
    removeEventListener(type: string, listener: (event: EventInterface) => void): void;
}

Метод addEventListener, например, вызывается строкой и функцией, принимающей что-то вроде EventInterface.

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

  • Запретить использование типа и несовместимого прослушивателя
  • Ограничить типы
  • Для подсказки типа разработчики («вы можете использовать только эти типы»; «если вы используете этот тип, слушатель будет / должен быть этим типом», et c.)

Для этого я хочу разработчик для определения сопоставленного типа:

type EventsMap = {
    first: NiceEvent;
    second: AwesomeEvent;
}

Этот сопоставленный тип здесь для того, чтобы сказать: «Для типа события« first »он отправит« NiceEvent »». Это должен быть только тип, а не генерирующий код.

Итак, я изменил свой EventTargetInterface следующим образом:

interface EventTargetInterface<
    M extends {[key in keyof M]: EventInterface},
> {
    addEventListener<T extends keyof M>(type: T, listener: (event: M[T]) => void): void;
    dispatchEvent<T extends M[keyof M]>(event: T): boolean;
    removeEventListener<T extends keyof M>(type: T, listener: (event: M[T]) => void): void;
}

Пока все хорошо, это должно ограничить тип только ключи «данной» карты и слушатель будет привязан к ней. Но теперь, EventTargetInterface требует универсального c, поэтому я должен также изменить EventInterface:

interface EventInterface<
    M extends {[key in keyof M]: EventInterface<M>},
> {
    target: EventTargetInterface<M>;
    type: keyof M;
}

ОК, хорошо выглядит. Давайте теперь добавим базовую реализацию:

abstract class EventBase<
    M extends {[key in keyof M]: EventInterface<M>},
> implements EventInterface<M> {
    target: EventTargetInterface<M>;
    type: keyof M;

    constructor(type: keyof M) {
        this.type = type;
    }
}

abstract class EventTargetBase<
    M extends {[key in keyof M]: EventInterface<M>},
> implements EventTargetInterface<M> {
    addEventListener<T extends keyof M>(type: T, listener: (event: M[T]) => void): void {}
    dispatchEvent<T extends M[keyof M]>(event: T): boolean { return false; }
    removeEventListener<T extends keyof M>(type: T, listener: (event: M[T]) => void): void {}
}

А теперь первая конкретная реализация:

enum MyEvents {
    FIRST = 'first',
    SECOND = 'second',
}

type MyEventsMap = {
    [key in MyEvents]: MyEvent;
}

class MyEvent<
    M extends MyEventsMap = MyEventsMap,
> extends EventBase<M> {}

class MyEventTarget<
    M extends MyEventsMap = MyEventsMap,
> extends EventTargetBase<M> {}

И вот здесь у меня проблема (на extends EventBase<M> и EventTargetBase<M>):

Type 'M' does not satisfy the constraint '{ [key in keyof M]: EventInterface<M>; }'.
  Type 'MyEventsMap' is not assignable to type '{ [key in keyof M]: EventInterface<M>; }'.ts(2344)

Итак, для Typescript что-то расширяющее MyEventsMap не соответствует M extends {[key in keyof M]: EventInterface<M>}. Но я не понимаю, потому что MyEvent расширяет EventBase, который реализует EventInterface!

Еще более запутанно, используя:

class MyEvent extends EventBase<MyEventsMap> {}

Это хорошо для Typescript, поэтому я не не поймете, что происходит неправильно при использовании generi c. (Мне нужно сохранить шаблон c, так как я хочу, чтобы мой класс расширялся, но это еще одна топи c)

Вы можете получить доступ к Typescript Playground , если хотите поиграть с это.

Ответы [ 3 ]

0 голосов
/ 11 мая 2020

Я не уверен, что полностью понимаю конечную цель (возможно, помогут еще несколько примеров использования). Однако то, как я сейчас интерпретирую задачу ... это решает ее?

class Item<
    T extends string,
    K extends string = Categories,
> {
    category: T;
    items: Record<K, Item<T, K>>;
}
0 голосов
/ 11 мая 2020

Хорошо, простой способ получить набранную карту ключей:

type Categories = "SECOND" | "FIRST";

const myMap = new Map<Categories, string>();
myMap.set("FOURTH", "Fourth");
myMap.set("FIRST", "Fourth");
myMap.set("SECOND", "Fourth");

enter image description here

0 голосов
/ 10 мая 2020

UPD

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

Основная проблема, которая мешает мне сформулировать разумный ответ, - это не обобщение, но свойства объекта, которые производят неопределенное вложение. В основном это свойство target на интерфейсе EventInterface, каким оно должно быть? Это то же самое EventTargetInterface, которое отправило событие? Это другой EventTargetInterface? Как в обоих случаях инициализировать свойство target? Так что я действительно смог заставить ваш пример (вроде?) Работать в TS песочнице , , но вы должны действительно подумать об изменении своего дизайна каким-то образом. С этими циклическими зависимостями действительно трудно иметь дело, и я не знаю, будет ли это работать, как ожидается, во всех случаях, эти генерики очень нестабильны. Это может вызвать у разработчиков больше головной боли, чем помощи.

Причина ошибки

Таким образом, основная причина заключается в конце ошибки, например:

'MyEventsMap' is assignable to the constraint of type 'M', but 'M' could
be instantiated with a different subtype of constraint 'MyEventsMap'.

Например, , возьмите следующую функцию

function fn<T extends string | number>(): T {
  return "I don't work"
}

Это приведет к аналогичной ошибке

'"I don't work"' is assignable to the constraint of type 'T', but 'T'
could be instantiated with a different subtype of constraint 'string | number'.

Причина должна быть очевидна, T не гарантированно включает тип string, я мог бы также назовите это как fn<number>() или fn<1>(), и это не будет работать. В машинописи, которая причиняла мне много боли в шее, является тот факт, что вы не можете запретить вызов функции / создание экземпляра класса / использование псевдонима типа / сделать что-либо в отношении обобщений с подтипом противопоказания в extends пункте . Это не совсем верно, есть хак, использующий вспомогательные типы, такие как

type EnsureMyMap<T> = T extends MyEventsMap<infer M> ? MyEventsMap<M> : never

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

Честно говоря, не знаю, как это относится к вашему конкретному примеру, и у меня нет wi sh, чтобы в него вникать. Может быть, это из-за значения по умолчанию, которое вы указываете для generi c в MyEvent и MyEventTarget, но я не уверен. Вы, вероятно, могли бы исправить все с помощью этого ключевого слова infer и вспомогательных типов через час или два изо всех сил, но стоит ли это того?

Так что я бы предложил переосмыслить дизайн этих классов. В частности, было бы неплохо избавиться от свойства target события, которое вызывает все эти циклические зависимости. Может тебе это не нужно? Может быть, вы могли бы передать его вместе с событием? Другим вариантом будет go менее строго соблюдать ограничения и уменьшить головную боль в будущем за счет худших подсказок (я имею в виду полное избавление от этой карты событий). Я могу обновить ответ еще раз, если вам нужна помощь по переработке всего этого, мне просто нужен пример времени выполнения, а не только объявление окружающего типа. Это все, что я могу предложить сейчас.

Оригинальный ответ

Ваш код кажется немного странным. Есть много циркулярных ссылок. Во-первых, я предполагаю, что во втором фрагменте кода тип должен иметь другое имя, иначе ItemsMap, как предложено в комментариях Ини go, ссылается на себя:

type MyMap = {
    first: Item<Categories.FIRST, ItemsMap>;
    second: Item<Categories.SECOND, ItemsMap>;
}

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

const map: MyMap = { first: /*...*/, second: /*...*/ }

Так что же должно быть map.first? Объект типа

{ 
  category: Categories.FIRST, 
  items: /*...*/
}

Так что же такое map.first.items? ItemsMap, например { [Category.FIRST]: /*...*/ }. Так что же такое map.first.items[Category.FIRST]? Объект типа

{
  category: Category.FIRST,
  items: /* ... */
}

Теперь go назад к предыдущему абзацу и еще один [Category.FIRST].items к свойству, о котором я написал вопрос.

Так что это продолжается вечно. Он останавливается, если вы назначаете пустой объект для items в какой-то момент, но в любом случае не похоже, что вам нужно бесконечно вложенное дерево элементов и категорий. Извините, если вы это сделаете, это просто не понятно из вашего кода и объяснений.

Так что единственное, что я могу предположить, это то, что вы не указали, что использовать класс Item в определении ItemsMap. В этом случае это будет иметь некоторый смысл для меня. Вы только что заявили

Цель состоит в том, чтобы передать "карту" перечисления / типа

Так что я думаю, что вы хотите сделать объект примерно таким (если Item в ItemsMap определение заменяется на { id: key }, например):

const map = {
  first: {
    category: 'first',
    items: { 
      first: { id: 'first' },
      second: { id: 'second' },
    },
  },
  second: {
    category: 'second',
    items: {
      first: { id: 'first' },
      second: { id: 'second' },
    },
  },
}

Даже здесь для меня не имеет особого смысла, почему вы хотите, чтобы ваши ключи были от тот же набор в map объекте и во всех map[...].items объектах. Опять же, извините, если вы не это имели в виду, я обновлю свой ответ, если вы предоставите более точное объяснение ваших целей. Также покажите пример объекта того типа, который вы хотите определить.

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