Типы TypeScript

Я борюсь с тем, как сильно напечатать некоторые функции с помощью TypeScript.

По сути, у меня есть функция, которая принимает карту ключ / значение DataProviders и возвращает карту ключ / значение данных, возвращаемых от каждого. Вот упрощенная версия проблемы:

interface DataProvider<TData> {
    getData(): TData;
}

interface DataProviders {
    [name: string]: DataProvider<any>;
}

function getDataFromProviders<TDataProviders extends DataProviders>(
    providers: TDataProviders): any {

    const result = {};

    for (const name of Object.getOwnPropertyNames(providers)) {
        result[name] = providers[name].getData();
    }

    return result;
}

В настоящее время getDataFromProviders имеет возвращаемый тип any, но я хочу, чтобы при таком вызове ...

const values = getDataFromProviders({
    ten: { getData: () => 10 },
    greet: { getData: () => 'hi' }
});

... тогда values будет неявно строго типизирован как:

{
    ten: number;
    greet: string;
}

Я предполагаю, что это потребует возврата универсального типа с универсальным параметром TDataProviders, но я не могу с этим справиться.

Это лучшее, что я могу придумать, но не компилируется ...

type DataFromDataProvider<TDataProvider extends DataProvider<TData>> = TData;

type DataFromDataProviders<TDataProviders extends DataProviders> = {
    [K in keyof TDataProviders]: DataFromDataProvider<TDataProviders[K]>;
}

Я изо всех сил пытаюсь придумать тип DataFromDataProvider, который компилируется без явной передачи TData в качестве второго параметра, что я не думаю, что смогу сделать.

Любая помощь будет принята с благодарностью.


person CodeAndCats    schedule 05.06.2017    source источник
comment
Если количество поставщиков фиксировано и относительно невелико (‹= 10), лучшее, что я могу придумать, - это изменить getDataFromProviders на использование вариативных аргументов, использовать фантомный тип для кодирования ключа (десять, привет). Много шаблонов, но они дают то, что вам нужно: gist.github.com/evansb/7afc5ac7e640a0697059276f   -  person Evan Sebastian    schedule 05.06.2017
comment
Спасибо @EvanSebastian, это интересный подход. Я не знал, что можно написать [K in K2], где K2 - это нечто иное, чем массив. Результирующий тип - это именно то, что мне нужно, но код не работает. При таком подходе теряются физические имена поставщиков данных. Я мог бы заставить каждого провайдера возвращать свои имена, но я хочу, чтобы это было СУХОЕ.   -  person CodeAndCats    schedule 05.06.2017
comment
О, я просто скопировал вставленный ваш код, вам, очевидно, нужно его изменить. Я должен был оставить пустую реализацию, моя беда. Да, к сожалению, невозможно заставить каждый провайдер возвращать свой тип и получать от него строковый литерал. Я считаю, что для этого нужен экзистенциальный тип   -  person Evan Sebastian    schedule 05.06.2017
comment
Не стоит беспокоиться. Я подумал о [K1 in K2] немного больше и понял, что K2 не массив, а объединение строковых значений, и отдельная строка может считаться объединением только с одним значением в наборе. Теперь имеет больше смысла.   -  person CodeAndCats    schedule 06.06.2017


Ответы (1)


Представьте, что у вас есть тип, который сопоставляет имя поставщика с типом данных, возвращаемым поставщиком. Что-то вроде этого:

interface TValues {
    ten: number;
    greet: string;
}

Обратите внимание, что вам на самом деле не нужно определять этот тип, просто представьте, что он существует, и используйте его как универсальный параметр с именем TValues повсюду:

interface DataProvider<TData> {
    getData(): TData;
}

type DataProviders<TValues> = 
    {[name in keyof TValues]: DataProvider<TValues[name]>};


function getDataFromProviders<TValues>(
    providers: DataProviders<TValues>): TValues {

    const result = {};

    for (const name of Object.getOwnPropertyNames(providers)) {
        result[name] = providers[name].getData();
    }

    return result as TValues;
}


const values = getDataFromProviders({
    ten: { getData: () => 10 },
    greet: { getData: () => 'hi' }
});

волшебным образом (фактически, используя вывод из сопоставленных типов, как указал @ Aris2World), машинописный текст может определять правильные типы:

let n: number = values.ten;
let s: string = values.greet;

обновление: как указал автор вопроса, getDataFromProviders в приведенном выше коде на самом деле не проверяет соответствие каждого свойства объекта, который он получает, интерфейсу DataProvider.

Например, если getData написано с ошибкой, ошибки нет, просто пустой тип объекта выводится как тип возвращаемого значения getDataFromProviders (тем не менее, вы все равно получите сообщение об ошибке, когда попытаетесь получить доступ к результату).

const values = getDataFromProviders({ ten: { getDatam: () => 10 } });

//no error, "const values: {}" is inferred for values

Есть способ заставить машинописный текст обнаруживать эту ошибку раньше за счет дополнительной сложности определения типа DataProviders:

type DataProviders<TValues> = 
    {[name in keyof TValues]: DataProvider<TValues[name]>}
   & { [name: string]: DataProvider<{}> };

Пересечение с индексируемым типом добавляет требование, чтобы каждое свойство DataProviders должен быть совместим с DataProvider<{}>. Он использует тип пустого объекта {} в качестве универсального аргумента для DataProvider, потому что DataProvider имеет хорошее свойство, которое для любого типа данных T, DataProvider<T> совместимо с DataProvider<{}> - T является типом возвращаемого значения getData(), а любой тип совместим с типом пустого объекта {}.

person artem    schedule 11.06.2017
comment
Это действительно отличное решение! - person Aleksey L.; 11.06.2017
comment
Я считаю это хорошим продвинутым примером сопоставленных типов. Я думаю, что ваш пост будет идеальным, если вы можете заменить «волшебным образом» объяснением или ссылкой на документацию;) - person Aris2World; 12.06.2017
comment
Вывод типа при отсутствии универсальных шаблонов относительно хорошо документирован в руководстве . К сожалению, есть только один короткий абзац о выводе типа для здесь, а там нет никаких упоминаний о его пределах. Магия заключается в том, чтобы получить то, что вы хотите, оставаясь в недокументированных пределах - если вы слишком сильно нажимаете на компилятор, он начинает выводить пустой тип объекта {} или, что еще хуже, any, поэтому --noImplicitAny является обязательным, если вы полагаетесь на вывод типа. - person artem; 12.06.2017
comment
Спасибо @artem, отличная работа. Я заметил только одно несовершенство - компилятор, похоже, не заставляет потребителей getDataFromProviders передавать параметры в форме { [name: string]: DataProvider<any> }. Таким образом, вы можете передать орфографическую ошибку, например, { ten: { getDatam: () => 10 } }, и он компилируется, ошибка возникает только в том случае, если вы пытаетесь использовать результат типа result.ten, потому что TS считает, что теперь результат {}. Если компилятор выдает ошибку при вызове с неправильными параметрами, это было бы идеально! - person CodeAndCats; 13.06.2017
comment
Есть способ обнаружить орфографические ошибки раньше, однако не уверен, стоит ли это дополнительных сложностей. Я обновил ответ. - person artem; 13.06.2017
comment
@artem Я думаю, что эта ссылка может быть подходящей для случая Расширенные типы в частности, параграф под названием Mapped Types, пример с прокси, когда он показывает unproxify. - person Aris2World; 13.06.2017
comment
@ Aris2World, спасибо, я это пропустил. Я обновил ответ. - person artem; 13.06.2017
comment
@artem это именно то поведение, которое я искал. Вы действительно заставили меня задуматься, могу ли я применить это решение к другим аналогичным проблемам, которые у меня были с отображенными типами! Большое вам спасибо, награда заработана! ???? - person CodeAndCats; 14.06.2017
comment
@artem, мне любопытно, есть ли у вас какие-нибудь творческие идеи на stackoverflow.com/q/46596846/678505, которые, как мне кажется, поднимут сопоставленные типы TS даже на следующий уровень :-) - person Michael Zlatkovsky - Microsoft; 07.10.2017