Несовпадение типов typescript

Собственно, проблема в коде ниже. Не пойму, почему в первом случае возникает такая ошибка. При том, что во втором все нормально.

interface DataMap {
    key1: number[];
    key2: string[];
};

type DataHandler<K extends keyof DataMap> = {(value: DataMap[K]): void};

type Data<K extends keyof DataMap> = [K, DataHandler<K>];

const storage1: Data<keyof DataMap>[] = [];

const storage2: {[K in keyof DataMap]: DataHandler<K>[]} = {
    key1: [],
    key2: [],
};

function getData<K extends keyof DataMap>(key: K, handler: DataHandler<K>): void {
    storage1.push([key, handler]); // Type 'DataHandler<K>' is not assignable to type 'DataHandler<keyof DataMap>'. Type 'keyof DataMap' is not assignable to type 'K'.
    storage2[key].push(handler); // OK
}

Ответы (1 шт):

Автор решения: Pavel Mayorov

Проблема тут в следующем.

Если раскрыть все эти keyof и прочее, то у вас получается следующая картина:

type DataHandlerNumber = { (value: number): void };
type DataHandlerString = { (value: string): void };
type DataHandlerAll = { (value: number | string): void };

type Storage1Type = [ 'key1' | 'key2', DataHandlerAll ][];
type Storage2Type = {
    key1: DataHandlerNumber[],
    key2: DataHandlerString[],
};

Переменная storage2 хранит все обработчики отдельно, и с ней никаких проблем не возникает. А вот переменная storage1, судя по использованию, пытается хранить нечто вроде полиморфного типа, однако на самом деле хранит что-то другое.

В том виде, в котором сейчас вы написали типы, у Typescript нет информации что первый и второй элементы кортежа Data связаны друг с другом.

Посмотрим как ваш тип Data объявлен ещё раз, опустив неважные детали:

type Data<K> = [K, DataHandler<K>];
// Data<'key1' | 'key2'> = [ 'key1' | 'key2', DataHandler<'key1' | 'key2'> ]

Дженерики в Typescript не являются дистрибутивными относительно оператора |, и когда вы пишете Data<'key1' | 'key2'> - получается совершенно не то же самое, что Data<'key1'> | Data<'key2'>.

Как можно это исправить? Ну, например можно использовать условный оператор, поскольку он по построению дистрибутивен. Замечу, что условие в этом операторе всегда истинное, сам оператор нужен не ради условия, а ради дистрибутивности:

type Data<K> = K extends any ? [K, DataHandler<K>] : never;
// Data<'key1' | 'key2'> = [ 'key1', DataHandler<'key1'> ] | [ 'key2', DataHandler<'key2'> ];

Теперь структура данных выглядит именно так, как вам и нужно. Но есть следующая проблема: вы не можете воспользоваться ей в обобщённом коде. Смотрите:

function getData<K extends keyof DataMap>(key: K, handler: DataHandler<K>): void {
    storage1.push([key as K, handler]);
    // Type 'DataHandler<K>' is not assignable to type 'DataHandler<"key1"> | DataHandler<"key2">'
}

Проблема тут в том, что функцию getData в теории можно вызвать вот так: getData<'key1' | 'key2'>('key1', …);, и в итоге тип выражения [key as K, handler] окажется недопустимым.

К сожалению, у этой проблемы нет нормального решения. Система типов Typescript - это всё же больше набор трюков для типизации библиотек, а не система формальной верификации кода. Поэтому наилучшим из известных мне способов является просто отказ от типизации конкретно в этом месте:

function getData<K extends keyof DataMap>(key: K, handler: DataHandler<K>): void {
    storage1.push([key, handler as any]);
    storage2[key].push(handler);
}
→ Ссылка