web-dev-qa-db-ja.com

ジェネリックのマップに制約を追加するにはどうすればよいですか?

編集:私は十分に明確ではなかったので、次の例にジャンプしたいと思うかもしれません。

ジェネリクスの制約に関する問題があり、自分が間違っていることを理解できません。
基本的に、私はこれを試しています:

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;
}

目標は、enum/typeの「マップ」を渡すことです(アイテムは後で「type」を使用する必要があります)。

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

それでも、MジェネリックのデフォルトでTSエラーが発生しました。

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はバーでエラーを生成します:

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のような関数を使用して呼び出すことができます。

その時点から、いくつかの理由でいくつかの制約を追加したいと思います。

  • タイプと互換性のないリスナーの使用を禁止するには
  • タイプを制限するには
  • 開発者へのタイプヒント(「これらのタイプしか使用できない」、「このタイプを使用する場合、リスナーはこのタイプである/する必要がある」など)

そのためには、マップされたタイプを開発者に定義してもらいます。

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はジェネリックを受け取るため、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>}に準拠しません。 MyEventEventBaseを実装するEventInterfaceを拡張するので、わかりません!

さらに混乱して、以下を使用します:

class MyEvent extends EventBase<MyEventsMap> {}

TypeScriptの場合はこれで問題ないので、ジェネリックを使用しても問題が発生しません。 (クラスを拡張可能にしたいので、ジェネリックを維持する必要がありますが、それは別のトピックです)

TypeScript Playground にアクセスしたい場合は、それにアクセスできます。

3
Neovov

最終目標を完全に理解しているとは思いません(おそらく、さらにいくつかの使用例が役立つでしょう)。しかし、私が現在タスクを解釈する方法...これはそれを解決しますか?

class Item<
    T extends string,
    K extends string = Categories,
> {
    category: T;
    items: Record<K, Item<T, K>>;
}
0
amakhrov

型付きキーマップを作成する簡単な方法は次のとおりです。

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
Ernesto