Есть ли способ компилировать такой код и быть безопасным?
type ComplexObject = {
primitive1: boolean;
complex: {
primitive2: string;
primitive3: boolean;
}
};
interface MyReference {
myKey: keyof ComplexObject;
}
const works1: MyReference = {
myKey: "primitive1"
}
const works2: MyReference = {
myKey: "complex"
}
const iWantThisToCompile1: MyReference = {
myKey: "complex.primitive2" // Error: Type '"complex.primitive2"' is not assignable to type '"primitive1" | "complex"'.
}
const iWantThisToCompile2: MyReference = {
myKey: "complex['primitive3']" // Error: Type '"complex['primitive3']"' is not assignable to type '"primitive1" | "complex"'.
}
// const iDontWantThisToCompile1: MyReference = {
// myKey: "primitive2"
// }
// const iDontWantThisToCompile2: MyReference = {
// myKey: "primitive3"
// }
Вы можете поиграть с этим кодом здесь.
Нет, к сожалению, Typescript не может этого сделать.
Редактировать: TS 4.1 добавил литералы шаблонов , см. ответ Дэвида Шеррета о том, как использовать их в рекурсивном типе
Единственное, что он поддерживает, это рекурсивный массив путей:
type Cons<H, T> = T extends readonly any[] ?
((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
: never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
P extends [] ? never : Cons<K, P> : never
) }[keyof T]
: [];
type ComplexObject = {
primitive1: boolean;
complex: {
primitive2: string;
primitive3: boolean;
}
};
interface MyReference {
myKey: Paths<ComplexObject>;
}
const works1: MyReference = {
myKey: ["primitive1"]
}
const works2: MyReference = {
myKey: ["complex"]
}
const iWantThisToCompile1: MyReference = {
myKey: ["complex", "primitive2"]
}
const iWantThisToCompile2: MyReference = {
myKey: ["complex", "primitive3"]
}
Библиотеки, такие как get
lodash, работают как с вашим "complex.primitive2"
, так и с массивом путей, таких как ["complex", "primitive2"]
. Поэтому, хотя это может быть не точный ответ, который вы ищете, надеюсь, он даст вам более безопасную альтернативу.
Я не думаю, что это точная копия, но вот ответ, из которого я получил псевдоним типа Paths
: Определение типа TypeScript для пути к свойству объекта
Оформить заказ Условные типы. Тип Paths
активно их использует, и я уверен, что есть способ узнать, является ли тип значения расширением boolean
, или использовать never
, если нет. Я только что попытался изменить его сам, но не смог понять это...
Это возможно благодаря новым литеральным типам шаблонов и рекурсивным типам в TypeScript 4.1.
Вот способ определения этого, который работает за пределами одного уровня. Можно использовать меньше типов, но этот подход не имеет дополнительных неиспользуемых параметров типа в общедоступном API.
export type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
}[keyof TObj & (string | number)];
type RecursiveKeyOfInner<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];
type RecursiveKeyOfHandleValue<TValue, Text extends string> =
TValue extends any[] ? Text :
TValue extends object
? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
: Text;
Если вам просто нужен доступ к свойству, это намного проще:
export type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
TObj[TKey] extends any[] ? `${TKey}` :
TObj[TKey] extends object
? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
: `${TKey}`;
}[keyof TObj & (string | number)];
export type RecursiveKeyOf<TObj extends object> = (
(
// Create an object type from `TObj`, where all the individual
// properties are mapped to a string type if the value is not an object
// or union of string types containing the current and descendant
// possibilities when it's an object type.
{
// Does this for every property in `TObj` that is a string or number
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
}
)[
keyof TObj & (string | number) // for every string or number property name
] // Now flatten the object's property types to a final union type
);
// This type does the same as `RecursiveKeyOf`, but since
// we're handling nested properties at this point, it creates
// the strings for property access and index access
type RecursiveKeyOfInner<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];
type RecursiveKeyOfHandleValue<TValue, Text extends string> =
// If the value is an array then ignore it, providing back
// only the passed in text
TValue extends any[] ? Text :
// If the value is an object...
TValue extends object
// Then...
// 1. Return the current property name as a string
? Text
// 2. Return any nested property text concatenated to this text
| `${Text}${RecursiveKeyOfInner<TValue>}`
// Else, only return the current text as a string
: Text;
Например:
// this type
{
prop: { a: string; b: number; };
other: string;
}
// goes to
{
prop: "prop" | "prop.a" | "prop.b";
other: "other";
}
// goes to
"prop" | "prop.a" | "prop.b" | "other"
О, это круто, я пытался сделать конкатенацию строк в типе раньше, но я, должно быть, делал это неправильно. Ааа это было до 4.1
Вы можете объяснить, как это работает? в идеале объясняя сначала поддержку доступа к свойствам, так как это проще
@DanielKaplan Я добавил объяснение только для типа доступа к свойствам. Тип доступа к свойству и индексу следует аналогичным принципам.
Когда одно из свойств является массивом (например, имена: строка[]), это решение также рекурсивно обращается к методам массива, что нежелательно. То есть тип RecursiveKeyOf включает в себя имена.concat, имена.значения и т.д. Как можно этого не допустить?
@Marduk Я обновил пример, чтобы справиться с этим. Один из способов справиться с этим — проверить, является ли значение массивом условного типа, и выйти раньше, чем просматривать его свойства.
Когда вы ссылаетесь на такие типы, как Date
, то также можно ссылаться на primitive1.getDate
(демонстрация: tsplay.dev/mAjgRW). Это неправильная ссылка на ключ объекта.
привет @DavidSherret в webstorm intellisense я получаю отличный результат, но все же компилятор предупреждает меня, когда я использую вложенную строку, например, «user.phone.number», и по прямому ключу ее передачи. у вас есть идея, в чем может быть проблема?
«TS2589: экземпляр типа слишком глубокий и, возможно, бесконечный». - ТС 4.6.3
@DavidSherret, возможно ли извлечь окончательный тип данных для возвращаемого типа? То есть: function foo<T>(bar: RecursiveKeyOf<T>): FinalValueOfRecursiveKey<bar>
. По сути, если параметр bar
равен 'prop.a'
из вашего примера, я хочу, чтобы возвращаемый тип был string
О мой! Это определенно то, что я пытался сделать, но у меня не получилось. Большое спасибо! @ДэвидШеррет
Просто хотел зайти сюда и сказать, что если кто-то хочет включить элементы массива и его подразделы как 'arrayProp[0]' | 'arrayProp[0].a' | 'arrayProp[1]' |
'arrayProp[1].a' | ...` тогда просто замените extends any[] ? `${TKey}` :
на extends any[] ? `${TKey}` | `${TKey}[${number}]` | `${TKey}[${number}].${RecursiveKeyOf<TObj[TKey][number]>}` :
Это фантастическая работа. Единственный синтаксис Typescript, который я не понимаю (и не могу найти ссылку), — это когда вы сглаживаете тип объекта в его свойствах, используя { ... }[keyof TObj & (string | number)]
. Какую функцию Typescript вы там используете?
Это должно быть выполнимо с литералами шаблона:
type ComplexObject = {
primitive1: boolean;
complex: {
primitive2: string;
primitive3: boolean;
}
};
type PathOf<T> = {
[K in keyof T]: T[K] extends object ? K | `${K}.${PathOf<T[K]>}` | `${K}['${PathOf<T[K]>}']` : K
}[keyof T]
type PathOfComplexObject = PathOf<ComplexObject>
На игровой площадке есть некоторые жалобы, но если вы наведете курсор на PathOfComplexObject
, вы увидите сгенерированные типы. Я понимаю, почему он жалуется:
Реализация типа слишком глубокая и, возможно, бесконечная.
но я не уверен в:
Тип «K» нельзя присвоить типу «string | номер | большой | логический | ноль | неопределенный'.
PathOf
все равно потребуется немного больше внимания, если вы хотите сохранить обозначение скобок, поскольку добавление еще одного слоя к .complex
может привести к типу complex['primitive2.foo']
и другим.
По-видимому, пересечение K
с string
решает проблему not assignable to
.
Я получил помощь в другом месте и получил этот тип:
type ComplexObject = {
primitive1: boolean;
complex: {
primitive2: string;
primitive3: boolean;
}
};
type RecKeyof<T, Prefix extends string = never> =
T extends string | number | bigint | boolean
| null | undefined | ((...args: any) => any ) ? never : {
[K in keyof T & string]: [Prefix] extends [never]
? K | `['${K}']` | RecKeyof<T[K], K>
: `${Prefix}.${K}` | `${Prefix}['${K}']` | RecKeyof<T[K],`${Prefix}.${K}` | `${Prefix}['${K}']`>
}[keyof T & string];
interface MyReference {
myKey: RecKeyof<ComplexObject>;
}
const works1: MyReference = {
myKey: "primitive1"
}
const works2: MyReference = {
myKey: "complex"
}
const iWantThisToCompile1: MyReference = {
myKey: "complex.primitive2"
}
const iWantThisToCompile2: MyReference = {
myKey: "complex['primitive3']"
}
// const iDontWantThisToCompile1: MyReference = {
// myKey: "primitive2"
// }
// const iDontWantThisToCompile2: MyReference = {
// myKey: "primitive3"
// }
Здесь вы можете увидеть, как это работает.
Вот тип с лучшей документацией:
type RecKeyof<T, Prefix extends string = ""> =
// If T matches any of the types in the union below, we don't care about its properties.
// We must exclude functions, otherwise we get infinite recursion 'cause functions have
// properties that are functions: i.e. myFunc.call.call.call.call.call.call...
T extends string | number | bigint | boolean | null | undefined | ((...args: any) => any )
? never // skip T if it matches
// If T doesn't match, we care about it's properties. We use a mapped type to rewrite
// T.
// If T = { foo: { bar: string } }, then this mapped type produces
// { foo: "foo" | "foo.bar" }
: {
// For each property on T, we remap the value with
[K in keyof T & string]:
// either the current prefix.key or a child of prefix.key.
`${Prefix}${K}` | RecKeyof<T[K],`${Prefix}${K}.`>
// Once we've mapped T, we only care about the values of its properties
// so we tell typescript to produce the union of the mapped types keys.
// { foo: "1", bar: "2" }["foo" | "bar"] generates "1" | "2"
}[keyof T & string];
Это круто, я думаю, что это работает для моих нужд. Есть ли способ сказать, что путь ограничен свойствами, которые имеют логический тип?