Ладно, это длинно и очень конкретно. Был бы очень признателен за любой вклад.
Я написал рекурсивный тип Omit, который принимает тип T и кортеж строк («путь») и индексируется в T, удаляя последний элемент пути и возвращая этот тип.
Я уже смотрел на Глубокий пропуск с машинописным текстом, но это связано с рекурсивным пропуском одного и того же ключа, а не с перемещением по пути.
Я также просмотрел Тип Safe Omit Функция, но это относится к времени выполнения и относится к пропуску одного уровня.
Моя реализация:
// for going along the tuple that is the path
type Tail<T extends any[]> = ((...args: T) => void) extends ((head: unknown, ...rest: infer U) => void) ? U : never;
type DeepOmit<T, Path extends string[]> = T extends object ? {
0: Omit<T, Path[0]>;
1: { [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K] }
}[Path['length'] extends 1 ? 0 : 1] : T;
Я хочу сделать так, чтобы Path был допустимым обходом T, что-то вроде:
Path = [K1, K2, K3, ..., K_n] such that:
K1 extends keyof T, K2 extends keyof T[K1], ... Kn extends keyof[T][K1]...[K_(n-1)]
Первая попытка
Я написал тип, который принимает тип T и путь P и возвращает P, если он действителен, или never в противном случае:
type ExistingPathInObject<T, P extends any[]> = P extends [] ? P : {
0: ExistingPathInObject<T[P[0]], Tail<P>> extends never ? never : P;
1: never;
}[P[0] extends keyof T ? 0 : 1]
Однако в подписи DeepOmit я не могу применить Path extends ExistingPathInObject<T,Path>, поскольку это круговая зависимость. Я упоминаю об этой попытке, потому что может быть способ обойти цикличность и использовать этот тип для проверки Path как допустимого обхода T.
Вторая попытка
Поскольку я не могу использовать Path для самоограничения, я вместо этого попытался сгенерировать объединение всех существующих путей в типе, а затем потребовать Path для его расширения. Лучшее, что я мог придумать, это:
type Paths<T> = {
0: { [K in keyof T]: T[K] extends object ? [K, Paths<T[K]>[keyof Paths<T[K]>]] : [K] }
1: []
}[T extends object ? 0 : 1];
// an example type to test on
type HasNested = { a: string; b: { c: number; d: string } };
type pathTest = Paths<HasNested>;
//{
// a: ["a"];
// b: ["b", ["d"] | ["c"]];
//}
type pathTestUnion = Paths<HasNested>[keyof Paths<HasNested>]
// ["a"] | ["b", ["d"] | ["c"]]
Это позволяет мне сопоставить путь, записанный в виде дерева: ['a'] extends pathTestUnion и ['b', ['d']] extends pathTestUnion оба верны. Мне нужно добавить [keyof T], чтобы получить объединение, и я не могу поместить его в сам тип Paths, потому что он не распознан как действительный.
Сделав все это, я теперь с трудом переписываю DeepOmit, чтобы использовать это ограничение. Вот что я пробовал:
type Types<T> = T[keyof T];
type TypeSafeDeepOmit<T, Path extends Types<Paths<T>>, K extends keyof T> =
Path extends any[] ?
T extends object ?
{ // T is object and Path is any[]
0: Omit<T, K>;
1: { [P in keyof T]: P extends Path[0] ? TypeSafeDeepOmit<T[P], Path[1], Path[1][0]> : T[P] }
}[Path['length'] extends 1 ? 0 : 1] :
T : // if T is not object
never; // if Path is not any[]
type TSDO_Helper<T, P extends Types<Paths<T>>> = P extends any[] ? P[0] extends keyof T ? TypeSafeDeepOmit<T, P, P[0]> : never : never;
Это уродливо и использует вспомогательный тип, чтобы действительно работать. Я также должен сообщить компилятору, что P extends any[] и P[0] extends keyof T, хотя именно это и предназначено для обеспечения Paths. Я также получаю сообщение об ошибке при рекурсивном вызове TypeSafeDeepOmit с использованием Path[1] -
Type 'any[] & Path' is not assignable to type '[]'.
Types of property 'length' are incompatible.
Type 'number' is not assignable to type '0'
Я исправил это, установив Path extends Types<Paths<T>> | [], но я не уверен, что это правильный способ.
В итоге
Итак, есть ли более приятный способ обеспечить допустимый путь? Можно ли также поддерживать объединение путей, чтобы опустить их все? Прямо сейчас результат, который я получаю, представляет собой объединение результатов различных исключений.
24 часа с момента обнаружения «хака» рекурсии до обнаружения того, что это не рекомендуется. О, по крайней мере, это был интересный эксперимент! Спасибо за ссылки и ваш вклад :)






Это слишком долго для комментария, но я не знаю, считается ли это ответом.
Вот действительно отвратительное преобразование (со многими возможными пограничными случаями) от объединения пропусков к множеству пропусков:
type NestedId<T, Z extends keyof any = keyof T> = (
[T] extends [object] ? { [K in Z]: K extends keyof T ? NestedId<T[K]> : never } : T
) extends infer P ? { [K in keyof P]: P[K] } : never;
type MultiDeepOmit<T, P extends string[]> = NestedId<P extends any ? DeepOmit<T, P> : never>
То, как это работает (если оно работает), состоит в том, чтобы взять союз, подобный {a: string, b: number} | {b: number, c: boolean}, и использовать только те ключи, которые существуют во всех составляющих союза: {b: number}. Это трудно сделать, и это нарушает необязательные свойства и кто знает что еще.
Удачи, извините за отсутствие хорошего ответа.
Это интересно, мне нужно будет просмотреть разные части и убедиться, что я их понимаю. Похоже, в конце вы делаете вывод P только для того, чтобы реконструировать его, я полагаю, что это нечто большее. Обратите внимание, что в этой версии путь представляет собой строковый кортеж, а не вложенные пути, которые мне удалось сгенерировать.
Ну, вы можете изменить ограничение на P в MultiDeepOmit, не влияя на то, как это работает. В NestedId грузовик вывода и реконструкции предлагает компилятору вывести конкретный тип объекта, если это возможно, вместо набора псевдонимов типов.
Я заменил NestedId на extends infer P ? P ... и не увидел других результатов для нескольких базовых типов, которые я создал для игры. Можете ли вы объяснить, как выглядели бы иначе сгенерированные псевдонимы типов? К сожалению, я не могу проверить это некоторое время сейчас.
Если вы собираетесь это сделать, вы можете просто заменить (XXX) extends infer P ? P : never на XXX. Это тоже хорошо. Кажется, действительно зависит от среды, которую я использую, показывает ли IntelliSense что-то вроде {a: NestedId<{c: string}>, b: NestedId<{d: number}>} или {a: {c: string}, b: {d: number}}. Трюк с (XXX) extends infer P ? {[K in keyof P]: P[K]} : never-картой обычно заставляет меня использовать последнее во всех случаях, но если в вашей среде это не нужно, я бы не стал об этом беспокоиться.
Да, очевидно, нет необходимости выводить P, если я не манипулирую им, это был просто тест, чтобы увидеть, как будут выглядеть эти псевдонимы в отличие от конкретного типа. Я думаю, что сталкивался с этой проблемой (например, видеть Pick<{...},'c'> вместо результирующего типа), так что это полезный трюк, который нужно знать!
хех, это беспорядок ?. Обычно в таких случаях я просто выбираю максимальную глубину (4 или 5 уровней) и разворачиваю свои «рекурсивные» типы до этой глубины, а затем выручаю. Сейчас нет поддерживаемый метод для таких рекурсивных типов. В настоящее время официальное слово не делать то, что ты делаешь с
type R<T> = { 0: X, 1: F<R<T>> }[T extends Y ? 0 : 1]. Я не знаю, что я мог бы придумать что-нибудь менее уродливое, чем то, что у вас есть.