Typescript: вывод типа для объектов внутри массива

#typescript #type-inference

#typescript #вывод типа

Вопрос:

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

По сути, мой конструктор получает массив, содержащий список объектов, которые содержат объявление (своего рода) плагина и «пользовательскую конфигурацию» для плагина, каждого из них.

Мне нужен Typescript, чтобы убедиться, что предоставленные customConfig совпадают с типами на defaultConfig , однако мне не повезло, даже не приблизившись к нему.

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

Я надеюсь, что кто-нибудь может дать некоторый свет

 
type Entry = {
    extension: {
        defaultConfig: Record<PropertyKey, unknown>
        install: any
        uninstall: any
    },
    customConfig: Record<PropertyKey, unknown>
}


function initExtensions<I extends Entry[]>(a: I): void { /* ... */ }


initExtensions([
    {
        extension: {
            defaultConfig: { foo: true },
            install: () => {/* ... */ },
            uninstall: () => {/* ... */ },
        },
        customConfig: { foo: true } // <-- SHOULD BE OK
    },
    {
        extension: {
            defaultConfig: { bar: 123 },
            install: () => {/* ... */ },
            uninstall: () => {/* ... */ },
        },
        customConfig: { bar: true }  // <-- Should complain as should be a NUMBER
    },
])
 

Ответ №1:

Я надеюсь, что приведенное ниже решение достигает почти того, что вы ищете.

 // keeping defaultConfig and customConfig as seperate generics makes TS inference more granular
type EntryRaw<C extends {}, U extends {}> = {
    extension: {
        defaultConfig: C
        install: any
        uninstall: any
    },
    customConfig: U
}

// checks if configs are equal types wise
type ValidEntry<E extends EntryRaw<{}, {}>> = E extends EntryRaw<infer C, infer U> ? C extends U ? U extends C ? E : never : never : never


type ValidEntries<ES extends EntryRaw<{}, {}>[]> =
    ES extends [] ? ES : // recursion condition
    ES extends [infer E, ...infer R] ? // destruct
    E extends EntryRaw<{}, {}> ? // auxiliary check to allow ValidEntry check
    R extends EntryRaw<{}, {}>[] ? // auxiliary check to allow recursive 'call'
    E extends ValidEntry<E> ?
        [E, ...ValidEntries<R>] : // entry ok, recursive 'call'
        // some hacky error reporting
        [{ __INCOMPATABLE_CONFIG__: [E['extension']['defaultConfig'], 'vs', E['customConfig']] } amp; Omit<E, 'customConfig'>, ...ValidEntries<R>] 
            : never  : never  : never


// I have been not able to make TS happy with single array argument 
function initExtensions<ES extends EntryRaw<{}, {}>[]>(...es: ValidEntries<ES>): void {  
 }

initExtensions(
    {
        extension: {
            defaultConfig: { foo: true },
            install: () => {/* ... */ },
            uninstall: () => {/* ... */ },
        },
        customConfig: { foo: true } // <-- SHOULD BE OK
    },
    {
        extension: {
            defaultConfig: { bar: 123 },
            install: () => {/* ... */ },
            uninstall: () => {/* ... */ },
        },
        customConfig: { bar: true }  // <-- Should complain as should be a NUMBER
    },
)
 

ИГРОВАЯ ПЛОЩАДКА

Комментарии:

1. Ваше решение, похоже, частично работает. Однако элемент, который не проходит проверку, является extension элементом, а customConfig не … Я пытался выполнить проверку другим способом, но безуспешно. Есть идеи?

2. Обновлено, чтобы лучше определить проблему и расставить акценты customConfig .

3. Просто удивительно! Я попытаюсь изучить ваши типы и посмотреть, смогу ли я понять, как вы сделали эту работу, чтобы адаптировать ее к моему реальному сценарию. Большое спасибо. И последний вопрос, что такое «vs», который у вас есть в типе ValidEntries?

4.Это vs совершенно необязательно. Это означало ожидаемое по сравнению с предоставленным; в целом __INCOMPATABLE_CONFIG__ это своего рода хак, чтобы сообщить о несоответствии. Вероятно, вы могли бы изменить его на: { __INCOMPATABLE_CUSTOM_CONFIG__: E['customConfig'] } или вообще удалить.

Ответ №2:

На самом деле вы не можете делать именно то, что ищете, потому что TypeScript не поддерживает экзистенциальные типы. https://github.com/Microsoft/TypeScript/issues/14466

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

 type Entry<T, C extends T> = {
    extension: {
        defaultConfig: T
        install: any
        uninstall: any
    },
    customConfig: C
}

function asEntry<T, C extends T>(entry: Entry<T, C>) { return entry };

function initExtensions(entries: Entry<any, any>[]): void { /* ... */ }

initExtensions([
    asEntry({
        extension: {
            defaultConfig: { foo: true },
            install: () => {/* ... */ },
            uninstall: () => {/* ... */ },
        },
        customConfig: { foo: true } // <-- OK
    }),
    asEntry({
        extension: {
            defaultConfig: { bar: 123 },
            install: () => {/* ... */ },
            uninstall: () => {/* ... */ },
        },
        customConfig: { bar: true }  // <-- ERROR
    })
])
 

Комментарии:

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

2. Если вы его найдете, опубликуйте его. Я не видел лучшего решения.

Ответ №3:

Метод, который Тодд Скелтон демонстрирует в своем ответе, является единственным известным мне шаблонным решением. Проблема в том, что каждая запись в вашем входном массиве имеет другой тип (хотя и расширяется Entry ), который должен быть четко представлен в вашей сигнатуре универсальной функции, чтобы правильно проверять тип. Поскольку в настоящее время нет способа указать функцию с n несколькими параметрами шаблона, в итоге вы получите такие мерзости:

 type Entry<C> = {
    extension: { defaultConfig: C };
    customConfig: Partial<C>;
    install: any;
    uninstall: any;
};

type E<C> = Entry<C>;
function initExtensions<C1,C2,C3,C4,C5>(entries: [E<C1>,E<C2>,E<C3>,E<C4>,E<C5>]): void; 
function initExtensions<C1,C2,C3,C4>(entries: [E<C1>,E<C2>,E<C3>,E<C4>]): void; 
function initExtensions<C1,C2,C3>(entries: [E<C1>,E<C2>,E<C3>]): void; 
function initExtensions<C1,C2>(entries: [E<C1>,E<C2>]): void; 
function initExtensions<C1>(entries: [E<C1>]):void; 
function initExtensions(entries:E<any>[]): void {
    // Initialize your extensions here
}
 

Который на самом деле работает так, как вы хотите, до 5 записей, представляя входные данные не как массив одного типа, а как кортеж разной длины, заполненный уникальными типами. Это может выглядеть уродливо и странно, но на самом деле именно так библиотеки, подобные Lodash, вводят свои переменные функции, так что это своего рода необходимое зло.

Конечно, ничто не мешает вам генерировать до 50 отклонений этой подписи: она предназначена только для проверки типов и не отражается в вашем конечном выводе. Либо это, либо передача объектов конфигурации один за другим:

 function initExtension<C>(entry: Entry<C>): void {
    // Initialize one extension here
}

initExtension({
    extension: { defaultConfig: { foo: true } },
    customConfig: { foo: true },
    install: () => null,
    uninstall: () => null,
});
 

Что является простой альтернативой подходу Тоддса.