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






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