Как выйти из ситуации TS2615/циклической ссылки с пунктирными путями к свойствам?

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

Для этой цели предположим следующие объявления типов (заимствованные с небольшими изменениями из связанного ответа jcalz):

type PathsToStringProps<T> = T extends string ? [] : {
    [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>];

type Join<T extends any[], D extends string> =
    T extends [] ? never :
    T extends [infer F] ? F :
    T extends [infer F, ...infer R] ?
    F extends string ?
    `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string;

type DottedLanguageObjectStringPaths<T extends object> = Join<PathsToStringProps<T>, ".">;

Затем обратите внимание на следующие пользовательские типы, в которых используется указанный выше тип DottedLanguageObjectStringPaths<T>:

interface ITest {
    a: string;
}

interface IOptionBase<T extends object> {
    name: string;
    type: number;
    getFunc?: () => T;
    prop: DottedLanguageObjectStringPaths<T>;
}

interface IOption1<T extends object> extends IOptionBase<T> {
    type: 1;
}

interface IOption2<T extends object> extends IOptionBase<T> {
    type: 2;
}

type ActualOption<T extends object> = IOption1<T> | IOption2<T>;

type Wrapper<T extends object> = ActualOption<T> & ITest;

const x: Wrapper<ITest> = {
    type: 2,
    name: 'test',
    prop: 'a'
};

Все это работает безупречно. Однако, как только я добавляю ссылку на себя в ITest:

interface ITest {
    a: string;
    parent?: ITest;
}

литерал, присвоенный x, помечен как ошибка компилятора:

TS2615: Тип свойства parent циклически ссылается на себя в сопоставленном типе.

Я не уверен, как из этого выйти, так как и структура-обертка (которая в реальной жизни даже немного сложнее, чем в этом MWE), и родительская ссылка являются для нас обязательными требованиями.

Что делает это еще более странным, так это то, что если я ссылаюсь на себя через массив, проблем, похоже, снова не возникает:

interface ITest {
    a: string;
    parent?: ITest[];
}

Как я могу обойти (или хотя бы просто понять) это поведение компилятора TypeScript?

Вот Ссылка на игровую площадку всего этого.

Кем ты представляешь DottedLanguageEtc<ITest>? TS не поддерживает бесконечные объединения. Я не уверен, почему вы ожидаете здесь чего-то иного, кроме ошибки цикличности. Вам придется самостоятельно ограничить глубину, если вы напишете рекурсивный тип и запросите объединение всех его путей... может быть, что-то вроде эта ссылка на игровую площадку показывает. Это полностью решает вопрос или я что-то упускаю?

jcalz 29.05.2024 21:00

@jcalz: Объяснение таким образом имеет смысл ... особенно если я вспомню, что именно доступ к свойству входит в объединение, тогда как доступ по индексу (как это произошло бы с массивом, как показано в конце) не . Итак, в вашем модифицированном образце игровой площадки вы ограничиваете глубину? Выглядит многообещающе. На самом деле, я пытался исключить свойства типа T из PathsToStringProps<T>, но мне это не удалось.

F-H 29.05.2024 21:52
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
2
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Вы почти гарантированно получите ошибку цикличности, если напишете глубоко рекурсивный условный тип, например PathsToStringProps<T>, когда T сам является рекурсивным.

Как вы думаете, какой DottedLanguageObjectStringPaths<ITest> должен быть? Концептуально это будет бесконечный тип объединения "a" | "parent.a" | "parent.parent.a" | "parent.parent.parent.a" | ⋯. Но TypeScript не может представлять бесконечное объединение (объединения в TypeScript могут иметь тысячи или десятки тысяч членов, но это несколько меньше бесконечности), а также не может рекурсивно выполняться бесконечно (в зависимости от конструкции вы можете получить глубину в тысячу уровней, что тоже недостаточно велик).

Таким образом, ваше определение столкнется с проблемами. Надеюсь, это объясняет поведение и то, почему вы видите ошибку с упоминанием parent.


Что касается того, как это исправить: вам нужно решить, кем вы на самом деле хотите DottedLanguageObjectStringPaths<T> быть, когда T очень глубоко. Один из подходов — просто задать ограничение глубины. Может быть, вот так:

type PathsToStringProps<T, D extends number = 10, A extends any[] = []> =
    A['length'] extends D ? never :
    T extends string ? [] : {
        [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K], D, [0, ...A]>]
    }[Extract<keyof T, string>];

Здесь D — предполагаемый предел глубины (который я указываю по умолчанию как 10), а A — это аккумулятор кортеж , который вначале пуст. Каждый раз, когда мы рекурсивно обращаемся к PathsToStringProps, мы добавляем элемент в аккумулятор (через вариационные типы кортежей ), и как только length аккумулятора становится равным D, мы выходим из строя. (TypeScript не может увеличивать числовые литеральные типы, но может манипулировать кортежами и проверять их длину.)

Затем, когда вы напишете DottedLanguageObjectStringPaths<T> в терминах PathsToStringProps<T>, вы получите максимальную глубину по умолчанию 10 (которую вы можете изменить, если хотите):

type ComeOn = DottedLanguageObjectStringPaths<ITest>
/* type ComeOn = "a" | "parent.a" | "parent.parent.a" | "parent.parent.parent.a" | 
    "parent.parent.parent.parent.a" | "parent.parent.parent.parent.parent.a" | 
    "parent.parent.parent.parent.parent.parent.a" | 
    "parent.parent.parent.parent.parent.parent.parent.a" | 
    "parent.parent.parent.parent.parent.parent.parent.parent.a" */

И теперь ваша проблема исчезла:

const x: Wrapper<ITest> = {
    type: 2,
    name: 'test',
    prop: 'a',
    a: ""
};

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

type PathsToStringProps<T, U = never, V = T> =
    [V] extends [U] ? never :
    T extends string ? [] : {
        [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K], U | V>]
    }[Extract<keyof T, string>];

где U — это накопленное объединение уже увиденных типов, а V — это копия T (поскольку T extends string ? ⋯ — это дистрибутивный условный тип, и мы не хотим распределять T для внешней проверки; я не собираюсь отклоняться от темы далее здесь... достаточно сказать, что просто сложно писать надежные рекурсивные условные типы). При рекурсии мы добавляем текущую копию T к U. Если копию T когда-либо можно присвоить U, то это означает, что T — это то, что мы уже видели раньше (более или менее), и нам следует спастись.

Это дает нам

type ComeOn = DottedLanguageObjectStringPaths<ITest>
/* type ComeOn = "a" | "parent.a" */

который возвращается в parent ровно один раз.


Вам решать, используете ли вы один из них или какую-то их комбинацию или что-то еще. Точные сведения о том, когда и как рекурсия прекращается, также зависят от вас. Даже с ограничениями глубины или детекторами округлости вы можете написать что-то, что взорвется в других, немного отличающихся ситуациях. Глубоко рекурсивные условные типы склонны иметь причудливые крайние случаи. Об этом следует помнить, решая, действительно ли вы хотите использовать такой тип.

Детская площадка, ссылка на код

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

Невозможно включить телевизор Samsung Tizen с помощью предоставленных API
JavaScript Array Каждый метод передает аргумент, показывающий undefine для функции обратного вызова лямбда стрелки
Модуль Next.Js не найден: невозможно разрешить проблему после изменения пути
Инструмент «Пропустить тип» решает проблему необязательных атрибутов
API драматурга получает машинописный текст неопределенного значения
Панель навигации с размытием не показывает эффект, когда за ней находится элемент с каким-либо типом анимации?
Как имитировать только данные, за исключением другой информации (isLoading, isError и т. д.), возвращаемой из пользовательского перехватчика с помощью useQuery?
Использовать вывод типа, чтобы функция возвращала только определенное значение?
Фильтрация ненулевого значения из массива в typectipt
TypeScript больше не может правильно проверять существование свойств, как только проверки передаются функции?