Typescript — безопасный ввод текста с глубоким опусканием, или: как составить список допустимых путей к объектам

Ладно, это длинно и очень конкретно. Был бы очень признателен за любой вклад.

Я написал рекурсивный тип 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>> | [], но я не уверен, что это правильный способ.

В итоге

Итак, есть ли более приятный способ обеспечить допустимый путь? Можно ли также поддерживать объединение путей, чтобы опустить их все? Прямо сейчас результат, который я получаю, представляет собой объединение результатов различных исключений.

хех, это беспорядок ?. Обычно в таких случаях я просто выбираю максимальную глубину (4 или 5 уровней) и разворачиваю свои «рекурсивные» типы до этой глубины, а затем выручаю. Сейчас нет поддерживаемый метод для таких рекурсивных типов. В настоящее время официальное слово не делать то, что ты делаешь с type R<T> = { 0: X, 1: F<R<T>> }[T extends Y ? 0 : 1]. Я не знаю, что я мог бы придумать что-нибудь менее уродливое, чем то, что у вас есть.

jcalz 13.05.2019 21:29

24 часа с момента обнаружения «хака» рекурсии до обнаружения того, что это не рекомендуется. О, по крайней мере, это был интересный эксперимент! Спасибо за ссылки и ваш вклад :)

Ran Lottem 13.05.2019 21:42
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
3
2
1 212
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это слишком долго для комментария, но я не знаю, считается ли это ответом.

Вот действительно отвратительное преобразование (со многими возможными пограничными случаями) от объединения пропусков к множеству пропусков:

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 только для того, чтобы реконструировать его, я полагаю, что это нечто большее. Обратите внимание, что в этой версии путь представляет собой строковый кортеж, а не вложенные пути, которые мне удалось сгенерировать.

Ran Lottem 13.05.2019 22:06

Ну, вы можете изменить ограничение на P в MultiDeepOmit, не влияя на то, как это работает. В NestedId грузовик вывода и реконструкции предлагает компилятору вывести конкретный тип объекта, если это возможно, вместо набора псевдонимов типов.

jcalz 14.05.2019 01:11

Я заменил NestedId на extends infer P ? P ... и не увидел других результатов для нескольких базовых типов, которые я создал для игры. Можете ли вы объяснить, как выглядели бы иначе сгенерированные псевдонимы типов? К сожалению, я не могу проверить это некоторое время сейчас.

Ran Lottem 14.05.2019 08:02

Если вы собираетесь это сделать, вы можете просто заменить (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-картой обычно заставляет меня использовать последнее во всех случаях, но если в вашей среде это не нужно, я бы не стал об этом беспокоиться.

jcalz 14.05.2019 15:31

Да, очевидно, нет необходимости выводить P, если я не манипулирую им, это был просто тест, чтобы увидеть, как будут выглядеть эти псевдонимы в отличие от конкретного типа. Я думаю, что сталкивался с этой проблемой (например, видеть Pick<{...},'c'> вместо результирующего типа), так что это полезный трюк, который нужно знать!

Ran Lottem 14.05.2019 16:53

Другие вопросы по теме