typescript - Typescript - 接口/类型 - 重新排列对象属性及其子元素在同一个键上,用点分隔
问题描述
英语不是我的第一语言。
我正在尝试创建一个接口/类型来验证传递的值是否是另一个接口的变体。
验证对象是否有效:
const obj1 = {Ab: {Cd: 1}};
也适用于:
const obj2 = {"Object.Ab.Cd": 1};
目前,我能得到的最接近的是这个。
返回类型现在不是那么重要,现在,我的重点是属性。
这是我目前的代码:
type RecursiveObjectFilter<T> = {
[P in keyof T as `.${Capitalize<string & RecursiveObjectFilter<P>>}`]: RecursiveObjectFilter<P>;
};
type Validate<T> = T extends string | number | bigint | boolean ? T : RecursiveObjectFilter<T>;
export interface IFilterBack<T> {
Object?: {
[P in keyof T as `Object${Capitalize<string & Validate<P>>}`]?: Validate<P>;
};
PageNumber?: number;
RowsPerPage?: number;
OrderByColumn?: string;
}
interface test {
Ab: { Cd: number };
}
const a: IFilterBack<test> = {
Object: { "Object.Ab.Cd": 1 }
};
解决方案
这个任务可以分成两个不太复杂的任务。首先,重命名对象键,并为其添加属性的完整路径。然后将物体深度压平。
我相信重命名键是这里最简单的部分:
type RemapKeys<T, D extends number = 5, P extends string ="Object"> =
[D] extends [never] ? never : T extends object ?
{ [K in keyof T as K extends string ? `${P}.${K}` : never]: RemapKeys<T[K], Prev[D], K extends string ? `${P}.${K}` : never> } : T
在这里,我们只保留一些前缀P
,当遇到任何对象调用RemapKeys
递归时,新前缀由先前的P
值和我们正在迭代的对象的键组成。我们在这里使用映射类型、键重映射和递归条件类型。
重命名后我们的新类型结构如下:
interface test {
Ab: { Cd: number, Ef: { Gh: string } };
Ij: boolean;
}
/*
type Remapped = {
"Object.Ab": {
"Object.Ab.Cd": number;
"Object.Ab.Ef": {
"Object.Ab.Ef.Gh": string;
};
};
"Object.Ij": boolean;
}
*/
type Remapped = RemapKeys<test>
然后是更难的部分。展平物体。
所以扁平对象是由我们在根级别上的所有属性加上嵌套对象的所有属性组成的对象:
// root level properties
type NonObjectPropertiesOf<T> = {
[K in keyof T as T[K] extends object ? never : K]: T[K]
}
// nested object values
type ValuesOf<T> = T[keyof T];
type ObjectValuesOf<T> = Extract<ValuesOf<T>, object>
但是ObjectValuesOf
给了我们一个对象值的联合。虽然我们需要一个十字路口。这就是令人敬畏的UnionToIntersection
类型@jcalz
派上用场的地方。因此,对于一级嵌套对象,Flatten
类型可以写为:
type Flatten<T> = NonObjectPropertiesOf<T> & UnionToIntersection<ObjectValuesOf<T>>
/*
type FlattenOneLevelRemapped = {
"Object.Ij": boolean;
} & {
"Object.Ab.Cd": number;
"Object.Ab.Ef": {
"Object.Ab.Ef.Gh": string;
};
}
*/
type FlattenOneLevelRemapped = Flatten<Remapped>
但是对于深度嵌套的类型,我们需要递归。
type DeepFlatten<T, D extends number = 5> = [D] extends [never] ? never : T extends unknown
? NonObjectPropertiesOf<T> &
UnionToIntersection<DeepFlatten<ObjectValuesOf<T>, Prev[D]>>
: never;
/*
type DeepFlattenRemapped = {
"Object.Ij": boolean;
} & {
"Object.Ab.Cd": number;
} & {
"Object.Ab.Ef.Gh": string;
}
*/
type DeepFlattenRemapped = DeepFlatten<Remapped>
最后将它们组合在一起:
type IFilterBack<T> = {
Object: DeepFlatten<RemapKeys<T>>
}
interface test {
Ab: { Cd: number, Ef: { Gh: string } };
Ij: boolean;
}
const a: IFilterBack<test> = {
Object: {
"Object.Ab.Cd": 1,
"Object.Ij": true,
"Object.Ab.Ef.Gh": '',
}
};
我没有在这里考虑可以具有数组类型的属性,并且不确定这种类型是否能够很好地扩展以适应它们。
推荐阅读
- python-3.x - 如何在 asyncssh.run("sudo su") 上输入密码并继续输入命令?
- c# - URL 中的 ServiceStack 会话 ID
- typescript - 记录之间的区别
和 [key: string]: 类型 - rust - 如何创建所有子命令都是可选的 StructOpt 命令
- ibm-mq - 命令 mqreply.c 超时
- vue.js - Chrome 调试器在断点处跳转到错误的文件
- mongodb - mongo 4.4 中是否存在 mongorestore.exe 文件?
- python - 从 Pandas 中的括号中提取
- r - 如何辨别哪些变量取决于分组变量?
- php - 如果在laravel中满足条件,如何跳过数据行