#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,
});
Что является простой альтернативой подходу Тоддса.