[英]Typescript same key but different type nested (keyof but nested)
[英]Typescript: deep keyof of a nested object, with related type
我正在寻找一种方法来拥有嵌套对象的所有键/值对。
(用于 MongoDB 点符号键/值类型的自动完成)
interface IPerson {
name: string;
age: number;
contact: {
address: string;
visitDate: Date;
}
}
这是我想要实现的目标,使其成为:
type TPerson = {
name: string;
age: number;
contact: { address: string; visitDate: Date; }
"contact.address": string;
"contact.visitDate": Date;
}
在这个答案中,我可以使用Leaves<IPerson>
获取密钥。 所以它变成了'name' | 'age' | 'contact.address' | 'contact.visitDate'
'name' | 'age' | 'contact.address' | 'contact.visitDate'
'name' | 'age' | 'contact.address' | 'contact.visitDate'
。
在@jcalz 的另一个答案中,我可以使用DeepIndex<IPerson, ...>
获得深度相关的值类型。
是否可以将它们组合在一起,成为像TPerson
这样的TPerson
?
当我开始这个问题时,我认为它可以像[K in keyof T]: T[K];
,经过一些巧妙的改造。 但是我错了。 这是我需要的:
所以界面
interface IPerson {
contact: {
address: string;
visitDate: Date;
}[]
}
变成
type TPerson = {
[x: `contact.${number}.address`]: string;
[x: `contact.${number}.visitDate`]: Date;
contact: {
address: string;
visitDate: Date;
}[];
}
无需检查有效number
,数组/索引签名的性质应该允许任意数量的元素。
界面
interface IPerson {
contact: [string, Date]
}
变成
type TPerson = {
[x: `contact.0`]: string;
[x: `contact.1`]: Date;
contact: [string, Date];
}
元组应该是关心有效索引号的那个。
readonly
属性应该从最终结构中删除。
interface IPerson {
readonly _id: string;
age: number;
readonly _created_date: Date;
}
变成
type TPerson = {
age: number;
}
用例是针对 MongoDB,在创建数据后不能修改_id
、 _created_date
。 _id: never
在这种情况下不起作用,因为它会阻止TPerson
的创建。
interface IPerson {
contact: {
address: string;
visitDate?: Date;
}[];
}
变成
type TPerson = {
[x: `contact.${number}.address`]: string;
[x: `contact.${number}.visitDate`]?: Date;
contact: {
address: string;
visitDate?: Date;
}[];
}
将可选标志带入转换结构就足够了。
interface IPerson {
contact: { address: string; } & { visitDate: Date; }
}
变成
type TPerson = {
[x: `contact.address`]: string;
[x: `contact.visitDate`]?: Date;
contact: { address: string; } & { visitDate: Date; }
}
界面
interface IPerson {
birth: Date;
}
变成
type TPerson = {
birth: Date;
}
不是
type TPerson = {
age: Date;
"age.toDateString": () => string;
"age.toTimeString": () => string;
"age.toLocaleDateString": {
...
}
我们可以给出一个类型列表作为结束节点。
为了实现这个目标,我们需要创建所有允许路径的排列。 例如:
type Structure = {
user: {
name: string,
surname: string
}
}
type BlackMagic<T>= T
// user.name | user.surname
type Result=BlackMagic<Structure>
对于数组和空元组,问题变得更有趣。
元组,显式长度的数组,应该这样管理:
type Structure = {
user: {
arr: [1, 2],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>
逻辑是直截了当的。 但是我们如何处理number[]
呢? 不能保证索引1
存在。
我决定使用user.arr.${number}
。
type Structure = {
user: {
arr: number[],
}
}
type BlackMagic<T> = T
// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>
我们还有 1 个问题。 空元组。 具有零元素的数组 - []
。 我们需要允许索引吗? 我不知道。 我决定使用-1
。
type Structure = {
user: {
arr: [],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>
我认为这里最重要的是一些约定。 我们也可以使用字符串化的“从不”。 我认为这取决于 OP 如何处理它。
既然我们知道我们需要如何处理不同的情况,我们就可以开始我们的实现了。 在我们继续之前,我们需要定义几个助手。
type Values<T> = T[keyof T]
{
// 1 | "John"
type _ = Values<{ age: 1, name: 'John' }>
}
type IsNever<T> = [T] extends [never] ? true : false;
{
type _ = IsNever<never> // true
type __ = IsNever<true> // false
}
type IsTuple<T> =
(T extends Array<any> ?
(T['length'] extends number
? (number extends T['length']
? false
: true)
: true)
: false)
{
type _ = IsTuple<[1, 2]> // true
type __ = IsTuple<number[]> // false
type ___ = IsTuple<{ length: 2 }> // false
}
type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
type _ = IsEmptyTuple<[]> // true
type __ = IsEmptyTuple<[1]> // false
type ___ = IsEmptyTuple<number[]> // false
}
我认为命名和测试是不言自明的。 至少我想相信:D
现在,当我们拥有所有的 utils 时,我们可以定义我们的主要 util:
/**
* If Cache is empty return Prop without dot,
* to avoid ".user"
*/
type HandleDot<
Cache extends string,
Prop extends string | number
> =
Cache extends ''
? `${Prop}`
: `${Cache}.${Prop}`
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends string> = {
[Prop in keyof Obj]:
// concat previous Cacha and Prop
| HandleDot<Cache, Prop & string>
// with next Cache and Prop
| Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]
type Path<Obj, Cache extends string = ''> =
// if Obj is primitive
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<unknown>
// and is tuple
? (IsTuple<Obj> extends true
// and tuple is empty
? (IsEmptyTuple<Obj> extends true
// call recursively Path with `-1` as an allowed index
? Path<PropertyKey, HandleDot<Cache, -1>>
// if tuple is not empty we can handle it as regular object
: HandleObject<Obj, Cache>)
// if Obj is regular array call Path with union of all elements
: Path<Obj[number], HandleDot<Cache, number>>)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>)
)
// "user" | "user.arr" | `user.arr.${number}`
type Test = Extract<Path<Structure>, string>
有一个小问题。 我们不应该返回最高级别的道具,例如user
。 我们需要至少有一个点的路径。
有两种方式:
两个选项很容易实现。
使用dot (.)
获取所有道具:
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
虽然上面的 util 是可读和可维护的,但第二个有点难。 我们需要在Path
和HandleObject
提供额外的通用参数。 请参阅取自其他问题/文章的此示例:
type KeysUnion<T, Cache extends string = '', Level extends any[] = []> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`, [...Level, 1]>
: Level['length'] extends 1 // if it is a higher level - proceed
? KeysUnion<T[P], `${Cache}.${P}`, [...Level, 1]>
: Level['length'] extends 2 // stop on second level
? Cache | KeysUnion<T[P], `${Cache}`, [...Level, 1]>
: never
: never
}[keyof T]
老实说,我认为任何人都不容易阅读此内容。
我们还需要实施一件事。 我们需要通过计算路径获得一个值。
type Acc = Record<string, any>
type ReducerCallback<Accumulator extends Acc, El extends string> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Acc = {}
> =
// Key destructure
Keys extends `${infer Prop}.${infer Rest}`
// call Reducer with callback, just like in JS
? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
// this is the last part of path because no dot
: Keys extends `${infer Last}`
// call reducer with last part
? ReducerCallback<Accumulator, Last>
: never
{
type _ = Reducer<'user.arr', Structure> // []
type __ = Reducer<'user', Structure> // { arr: [] }
}
您可以在我的博客中找到有关使用Reduce
更多信息。
全码:
type Structure = {
user: {
tuple: [42],
emptyTuple: [],
array: { age: number }[]
}
}
type Values<T> = T[keyof T]
{
// 1 | "John"
type _ = Values<{ age: 1, name: 'John' }>
}
type IsNever<T> = [T] extends [never] ? true : false;
{
type _ = IsNever<never> // true
type __ = IsNever<true> // false
}
type IsTuple<T> =
(T extends Array<any> ?
(T['length'] extends number
? (number extends T['length']
? false
: true)
: true)
: false)
{
type _ = IsTuple<[1, 2]> // true
type __ = IsTuple<number[]> // false
type ___ = IsTuple<{ length: 2 }> // false
}
type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
type _ = IsEmptyTuple<[]> // true
type __ = IsEmptyTuple<[1]> // false
type ___ = IsEmptyTuple<number[]> // false
}
/**
* If Cache is empty return Prop without dot,
* to avoid ".user"
*/
type HandleDot<
Cache extends string,
Prop extends string | number
> =
Cache extends ''
? `${Prop}`
: `${Cache}.${Prop}`
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends string> = {
[Prop in keyof Obj]:
// concat previous Cacha and Prop
| HandleDot<Cache, Prop & string>
// with next Cache and Prop
| Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]
type Path<Obj, Cache extends string = ''> =
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<unknown>
// and is tuple
? (IsTuple<Obj> extends true
// and tuple is empty
? (IsEmptyTuple<Obj> extends true
// call recursively Path with `-1` as an allowed index
? Path<PropertyKey, HandleDot<Cache, -1>>
// if tuple is not empty we can handle it as regular object
: HandleObject<Obj, Cache>)
// if Obj is regular array call Path with union of all elements
: Path<Obj[number], HandleDot<Cache, number>>)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>)
)
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
// "user" | "user.arr" | `user.arr.${number}`
type Test = WithDot<Extract<Path<Structure>, string>>
type Acc = Record<string, any>
type ReducerCallback<Accumulator extends Acc, El extends string> =
El extends keyof Accumulator ? Accumulator[El] : El extends '-1' ? never : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Acc = {}
> =
// Key destructure
Keys extends `${infer Prop}.${infer Rest}`
// call Reducer with callback, just like in JS
? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
// this is the last part of path because no dot
: Keys extends `${infer Last}`
// call reducer with last part
? ReducerCallback<Accumulator, Last>
: never
{
type _ = Reducer<'user.arr', Structure> // []
type __ = Reducer<'user', Structure> // { arr: [] }
}
type BlackMagic<T> = T & {
[Prop in WithDot<Extract<Path<T>, string>>]: Reducer<Prop, T>
}
type Result = BlackMagic<Structure>
下面是我对Flatten<T, O>
的完整实现Flatten<T, O>
它将类型可能嵌套的T
转换为“扁平”版本,其键是通过原始T
的虚线路径。 O
类型是一个可选类型,您可以在其中指定一个(联合)对象类型以保持原样而不将它们展平。 在您的示例中,这只是Date
,但您可以有其他类型。
警告:它非常丑陋,而且可能很脆弱。 到处都有边缘情况。 组成它的部分涉及奇怪的类型操作,这些操作要么并不总是按照人们的预期进行,要么对于除最经验丰富的 TypeScript 老手之外的所有人来说都是不可理解的,或者两者兼而有之。
有鉴于此,除了可能“请不要这样做”之外,对这个问题没有“规范”的答案。 但我很高兴展示我的版本。
这里是:
type Flatten<T, O = never> = Writable<Cleanup<T>, O> extends infer U ?
U extends O ? U : U extends object ?
ValueOf<{ [K in keyof U]-?: (x: PrefixKeys<Flatten<U[K], O>, K, O>) => void }>
| ((x: U) => void) extends (x: infer I) => void ?
{ [K in keyof I]: I[K] } : never : U : never;
这里的基本方法是采用您的T
类型,如果它不是一个对象或者它扩展了O
,则按原样返回它。 否则,我们删除任何readonly
属性,并将任何数组或元组转换为没有所有数组方法(如push()
和map()
)的版本并获得U
。 然后我们将其中的每个属性展平。 我们有一个键K
和一个扁平属性Flatten<U[K]>
; 我们想在Flatten<U[K]>
的虚线路径前面添加键K
,当我们完成所有这些后,我们希望将这些扁平对象(也包括未扁平对象) 相交成为一个大对象.
请注意,说服编译器生成交集涉及逆变位置中的条件类型推断(请参阅将联合类型转换为交集类型),即(x: XXX) => void)
和extends (x: infer I) => void
件进来。它使编译器采用所有不同的XXX
值并将它们相交以获得I
。
虽然像{foo: string} & {bar: number} & {baz: boolean}
这样的交集是我们在概念上想要的,但它比等效的{foo: string; bar: number; baz: boolean}
{foo: string; bar: number; baz: boolean}
{foo: string; bar: number; baz: boolean}
所以我做一些更多的条件类型映射{ [K in keyof I]: I[K] }
而不是只是I
(看我如何可以看到一个打字稿类型的全扩展的合约? )。
此代码通常分布在 unions 上,因此可选属性可能最终会产生 unions(例如{a?: {b: string}}
可能会产生{"ab": string; a?: {b: string}} | {"a": undefined, a?: {b: string}}
,虽然这可能不是您想要的表示形式,但它应该可以工作(因为,例如,如果a
是可选的, "ab"
可能不作为键存在)。
Flatten
定义取决于辅助类型函数,我将在此处以各种级别的描述呈现这些函数:
type Writable<T, O> = T extends O ? T : {
[P in keyof T as IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>]: T[P]
}
type IfEquals<X, Y, A = X, B = never> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;
Writable<T, O>
返回一个删除了readonly
属性的T
版本(除非T extends O
在这种情况下我们不理会它)。 它来自TypeScript 条件类型——过滤掉只读属性/只选择需要的属性。
下一个:
type Cleanup<T> =
0 extends (1 & T) ? unknown :
T extends readonly any[] ?
(Exclude<keyof T, keyof any[]> extends never ?
{ [k: `${number}`]: T[number] } : Omit<T, keyof any[]>) : T;
Cleanup<T>
类型将any
类型转换为unknown
类型(因为any
类型操作any
非常混乱),将元组转换为具有单个数字键( "0"
和"1"
等)的对象,并将其他数组转换为只是一个索引签名。
下一个:
type PrefixKeys<V, K extends PropertyKey, O> =
V extends O ? { [P in K]: V } : V extends object ?
{ [P in keyof V as
`${Extract<K, string | number>}.${Extract<P, string | number>}`]: V[P] } :
{ [P in K]: V };
PrefixKeys<V, K, O>
将键K
PrefixKeys<V, K, O>
到V
的属性键中的路径......除非V
扩展O
或V
不是对象。 它使用模板文字类型来做到这一点。
最后:
type ValueOf<T> = T[keyof T]
将类型T
转换为其属性的联合。 请参见TypeScript 中是否有类似于 `keyof` 的 `valueof`? .
哇! 😅
所以,你去了。 您可以验证这与您陈述的用例的紧密程度。 但它非常复杂和脆弱,我真的不建议在没有大量测试的任何生产代码环境中使用它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.