首页 > 解决方案 > 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 }
};

标签: typescripttypes

解决方案


这个任务可以分成两个不太复杂的任务。首先,重命名对象键,并为其添加属性的完整路径。然后将物体深度压平。

我相信重命名键是这里最简单的部分:

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": '',
  }
};

游乐场链接


我没有在这里考虑可以具有数组类型的属性,并且不确定这种类型是否能够很好地扩展以适应它们。


推荐阅读