Я использую следующий тип объединения
interface TypeMap {
boolean: boolean;
number: number;
string: string;
}
interface Item {
id: string;
}
type FieldHeader<I extends Item> = {
[Type in keyof TypeMap]: {
compare: (a: TypeMap[Type], b: TypeMap[Type]) => number;
filter: (value: TypeMap[Type], input: TypeMap[Type]) => boolean;
get: (item: I) => TypeMap[Type] | null | undefined;
type: Type;
};
}[keyof TypeMap];
Параметр
type TableHeader<I extends Item> = Record<
string,
FieldHeader<I>
>;
type Filter<I extends Item, T extends TableHeader<I>> = {
[Field in keyof T]: TypeMap[T[Field]['type']]; // this isn't working
}
во-первых, тип свойств Filter выводится неправильно. Учитывая определение Item и TableHeader, я мог бы определить Filter с любым ключом свойства и любым значением без каких-либо ошибок, см. здесь (л. 44)
А если я поставлю
function applyFilter<I extends Item, T extends TableHeader<I>>(item: I, tableHeader: T, filter: Filter<I, T>) {
Object.entries(filter).forEach(([key, input]) => {
const value = tableHeader[key].get(item);
if (value != null) tableHeader[key].filter(value, input);
})
}
я получаю сообщение об ошибке
Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'.
Type 'string' is not assignable to type 'never'.(2345)
(см. здесь, л. 56) что не совсем удивительно, вопрос в том, как мне устранить эту ошибку?
Вам потребуется провести рефакторинг, чтобы убедить компилятор в том, что это безопасно, как описано в ms/TS#47109 . Похоже на эту версию; это полностью отвечает на ваш вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?
Спасибо вам всем. Я бы придерживался подхода @jcalz, было бы здорово, если бы вы написали ответ






TypeScript не может отслеживать корреляции между типами союзов ; если у вас есть один блок кода, такой как ваше тело обратного вызова forEach(), компилятор может проанализировать его только один раз. Если у вас есть несколько выражений в таком блоке кода, которые зависят от одного и того же значения типа объединения, компилятор обычно просто обрабатывает эти выражения как сами типы объединения, но обрабатывает их независимо. Таким образом, функция, зависящая от ключа объединения, станет объединением функций. И аргумент, который зависит от ключа объединения, станет объединением аргументов. Но вы не можете безопасно вызывать объединение функций с объединением аргументов; компилятор будет настаивать на пересечении аргументов . Это тема microsoft/TypeScript#30581.
Самый простой подход здесь — просто использовать утверждения типа, чтобы скрыть ошибки, потому что вы уверены, что делаете это правильно.
Однако, если вы хотите, чтобы компилятор проверял тип того, что вы делаете, вы можете выполнить довольно большой рефакторинг, чтобы использовать общие индексы в отображаемые типы , как описано в microsoft/TypeScript#47109 .
Этот код уже прошел один раунд такого рефакторинга, но теперь вам нужно сделать это для типа отображения T из ключей заголовка таблицы в конкретный тип поля. Мы должны сохранить FieldHeader общее в K extends keyof TypeMap, чтобы сделать это; это нормально, если FieldHeader<I> по умолчанию является объединением, но мы хотим иметь возможность выбирать конкретное K:
type FieldHeader<I extends Item, K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
compare: (a: TypeMap[P], b: TypeMap[P]) => number;
filter: (value: TypeMap[P], input: TypeMap[P]) => boolean;
get: (item: I) => TypeMap[P] | null | undefined;
type: P;
};
}[K];
И теперь ваши типы должны быть универсальными для этого отображения T:
type TableHeader<I extends Item, T extends Record<keyof T, keyof TypeMap>> = {
[K in keyof T]: FieldHeader<I, T[K]>
}
type Filter<I extends Item, T extends Record<keyof T, keyof TypeMap>> = {
[K in keyof T]: TypeMap[T[K]];
}
И поэтому вам нужно будет указать это при объявлении переменных этих типов (вы можете использовать служебные функции для вывода вместо ручного указания, но я не буду отвлекаться на этом):
const tableHeader: TableHeader<MyItem, { selected: "boolean" }> = {
selected: {
compare: (a, b) => +a - +b,
filter: (value, input) => value === input,
get: (video) => video.selected,
type: 'boolean',
},
};
const filter: Filter<MyItem, { selected: "boolean" }> = {
// ts should throw an error here, as type of "selected" should be 'boolean'
selected: "",
// also only props of "tableHeader" should be allowed
foo: 123
}
И, наконец, ваша функция applyFilter() также является универсальной в T, например:
function applyFilter<I extends Item, T extends Record<keyof T, keyof TypeMap>>(
item: I, tableHeader: TableHeader<I, T>, filter: Filter<I, T>
) {
(Object.keys(filter) as Array<keyof T>).forEach(<K extends keyof T>(key: K) => {
const input = filter[key];
const value = tableHeader[key].get(item);
if (value != null) tableHeader[key].filter(value, input);
});
}
Обратите внимание, что нам все еще нужно утверждение типа, чтобы компилятор увидел, что записи filter имеют какое-либо отношение к keyof T, и на самом деле мы не можем использовать Object.entries(), потому что это не позволит нам представить связь между K и Filter<I, T>[K]. Но в остальном это работает; компилятор может видеть, что input относится к типу TypeMap[T[K]], и что value относится к типу TypeMap[T[K]] (после проверки на нулевое значение), и что tableHeader[key].filter относится к типу (value: TypeMap[T[K]], input: TypeMap[T[K]]) => boolean, так что все типы проверяются.
Это классика. Пожалуйста, смотрите мою статью .
tableHeader[key].filterпроизводит объединение функций. Обычно это не то, что вы хотите. Обычно вам нужно пересечение функций. Пожалуйста, имейте в виду, что пересечения функций - это фактически перегрузка функций. Приведите пример того, как вы хотите вызвать эту функцию. Короче говоря, вам нужно изменить свой подход