我希望能够重新映射对象键'prefix.suffix': value{prefix: { suffix: value }},但我挣扎如何正确键入新的重新映射对象.

如何从带有圆点的键中获取带有嵌套对象的remapsidData,并保留值类型?

export namespace LocationSchemas {
  export type BasicFields = {
    name: FieldProps;
    region: Region;
    types: Types;
    "alt.number"?: FieldProps;
    "alt.centreNumber"?: FieldProps;
    "typeDetails.available": TypeDetails;
    "typeDetails.virtual": TypeDetails;
    "internal.name"?: FieldProps;
    "internal.type": FieldProps;
    "internal.format"?: FieldProps;
    "internal.status": InternalTradingStatus;
  };

  export type FieldProps = {
    visible: boolean;
    editable: boolean;
  };

  export type PossibleValueAs<T> = {
    value: T;
  };
  export type PossibleValueAsString = PossibleValueAs<string>;
  export type PossibleValueAsStringArray = PossibleValueAs<string[]>;
  export type PossibleValueAsIso = PossibleValueAs<{
    isoCountryCode: string;
    isoSubdivision?: string;
  }>;

  export type Types = FieldProps & {
    possibleValues: PossibleValueAsStringArray[];
    defaultValue?: PossibleValueAsStringArray[];
  };

  export type TypeDetails = FieldProps & { defaultValue: boolean };

  export type Region = FieldProps & {
    possibleValues: PossibleValueAsIso[];
    defaultValue?: PossibleValueAsIso;
  };

  export type InternalTradingStatus = {
    visible: boolean;
    editable: boolean;
    possibleValues: PossibleValueAsString[];
  };
}

const remapKeys = (fields: LocationSchemas.BasicFields) => {
  return Object.entries(fields).reduce((base, currentValue) => {
    const [key, value] = currentValue;

    if (key.includes(".")) {
      const [firstHalf, secondHalf] = key.split(".");

      base[firstHalf] = { ...base[firstHalf], [secondHalf]: value };
    } else {
      base[key] = value;
    }

    return base;
  }, {});
};

const data: LocationSchemas.BasicFields = {
    name: {
        visible: true,
        editable: true,
    },
    types: {
        visible: true,
        editable: true,
        possibleValues: [
            {
                value: ["Razor"],
            },
            {
                value: ["Deck"],
            },
        ],
    },
    region: {
        visible: true,
        editable: true,
        possibleValues: [
            {
                value: {
                    isoCountryCode: "GB",
                    isoSubdivision: "ENG",
                },
            },
            {
                value: {
                    isoCountryCode: "FR",
                },
            },
        ],
    },
    "alt.number": {
        visible: true,
        editable: true,
    },
    "alt.centreNumber": {
        visible: true,
        editable: true,
    },
    
    "typeDetails.available": {
        visible: true,
        editable: true,
        defaultValue: false,
    },
    "typeDetails.virtual": {
        visible: true,
        editable: true,
        defaultValue: false,
    },
    "internal.name": {
        visible: true,
        editable: true,
    },
    "internal.type": {
        visible: true,
        editable: true,
    },
    "internal.status": {
        visible: true,
        editable: true,
        possibleValues: [
            {
                value: "TRADING",
            },
            {
                value: "UNKNOWN",
            },
        ],
    },
};



const remappedData = remapKeys(data) // I want object with re-mapped keys

推荐答案

我假设您主要希望编写一个名为RemapKeys<T>的实用程序类型,在输入类型为T的情况下,它将是remapKeys()函数所需的输出类型.我不会担心remapKeys()的实现,也不会担心如何让编译器相信实现与类型匹配;您只需要自己判断实现,并使用类似type assertion的符号来告诉编译器它返回的值属于该类型.

请注意,这样的深度recursive types往往具有奇怪的边缘情况,特别是如果事先没有完全指定您希望的行为是什么.如果事实证明某些边缘情况行为是不可接受的,您可能会发现该类型需要完全重构,而不是细微的调整.因此,一定要针对预期的用例彻底测试这样的递归类型.


我对RemapKeys<T>人采取的一般方法是

  • 对于每个值为V的属性K,生成一个"嵌套Record类型"NestedRecord<K, V>,它类似于the Record<K, V> type,只是K中的每个点都会使其嵌套得更深.因此,对于属性{"a.b.c": string},它将生成类型{a: {b: {c: string}}}

  • 将这些嵌套的记录组合到单个对象中,由intersecting个记录组成;

  • 为了便于显示和理解,将此大交叉点折叠为单一对象类型.


首先,我们需要NestedRecord<K, V>个.它的基本版本是

type NestedRecord<K extends PropertyKey, V> =
  K extends `${infer K0}.${infer KR}` ?
    { [P in K0]: NestedRecord<KR, V> } :
    { [P in K]: V }

{"a.b.c":0}变成了{a:{b:{c:0}}}.但它并不能保存optional properties美元.我假设你想让{"a.b.c"?: string}变成{a?: {b?: {c?: string}}},其中 struct 的每个部分都是可选的.这可能并不完全代表事实,因为您可能有{a: {b: {c: string}}}{},并且可能不想支持像{a: {b: {}}这样的"部分"呈现路径,但它可能足够接近.此外,如果可以将undefined赋值给某个属性,我将假定该属性是可选的.严格地说,这不是同一件事,如果你想要精确的东西,你可以看看is there a way to get all required properties of a typescript object,但同样,这可能已经足够接近了.因此,也适用于可选选项的版本是

type NestedRecord<K extends PropertyKey, V> =
  K extends `${infer K0}.${infer KR}` ?
  undefined extends V ? { [P in K0]?: NestedRecord<KR, V> } : { [P in K0]: NestedRecord<KR, V> } :
  undefined extends V ? { [P in K]?: V } : { [P in K]: V }

现在,我们需要将所有这些嵌套记录合并到单个对象中.从概念上讲,你会把它们加在一起,所以{"a.b": string, "c.d": string}会变成大约{a:{b:string}} & {c:{d:string}}.我们可以通过使用与Transform union type to intersection type中描述的UnionToIntersection类似的技术来做到这一点.就像这样:

type RemapKeys<T extends object> =
  { [K in keyof T]: (x: NestedRecord<K, T[K]>) => void } extends
  Record<string, (x: infer I) => void> ? I : never;

基本上,我要做的是将这些嵌套记录转换为函数类型的参数,然后使用conditional type inference从该位置推断出一个类型.这会产生一个交叉点,而不是一个union.


最后,我们希望将其合并为单个对象类型,这样{a:{b:string}} & {c:{d:string}}就变成了{a: {b: string}; c: {d: string}}.为此,我将使用一个实用程序类型ExpandRecursively<T>,它将对象类型的属性递归到它们自己.这是在How can I see the full expanded contract of a Typescript type?描述的,看起来是这样的:

type ExpandRecursively<T> =
  T extends object ? { [K in keyof T]: ExpandRecursively<T[K]> } : T;

所以把这些放在一起就给了我们

type RemapKeys<T extends object> = ExpandRecursively<
  { [K in keyof T]: (x: NestedRecord<K, T[K]>) => void } extends
  Record<string, (x: infer I) => void> ? I : never
>;

好的,让我们来测试一下.我们先从

type BasicFields = {
  name: FieldProps;
  region: Region;
  types: Types;
  "alt.number"?: FieldProps;
  "alt.centreNumber"?: FieldProps;
  "typeDetails.available": TypeDetails;
  "typeDetails.virtual": TypeDetails;
  "internal.name"?: FieldProps;
  "internal.type": FieldProps;
  "internal.format"?: FieldProps;
  "internal.status": InternalTradingStatus;
};

然后在其上使用RemapKeys,这为我们提供了一个与以下内容等价的类型:

type RemappedFields = RemapKeys<BasicFields>
/* type RemappedFields = {
    name: FieldProps;
    region: Region;
    types: Types;
    alt?: {
        number?: FieldProps;
        centreNumber?: FieldProps;
    };
    typeDetails: {
        available: TypeDetails;
        virtual: TypeDetails;
    };
    internal: {
        name?: FieldProps;
        type: FieldProps;
        format?: FieldProps;
        status: InternalTradingStatus;
    };
} */

这看起来就是你想要的.请注意,alt属性是可选的,因为它的两个子属性也是可选的,但internal是必需的,因为至少需要它的一些子属性.当组合交叉点时,这是很自然的;交叉点的任何成员都需要的属性在结果中成为必需的.


这就是我使用的基本方法. 当然还有其他方法可以做到这一点,根据边缘情况和用例,可能会有更好的方法.彻底判断并在需要时进行更改.

Playground link to code

Typescript相关问答推荐

TypScript中的算法运算式

Angular 17 Select 错误core.mjs:6531错误错误:NG 05105:发现意外合成监听器@transformPanel.done

来自类型脚本中可选对象的关联联合类型

在函数中打字推断记录的关键字

Cypress页面对象模型模式.扩展Elements属性

如何使用Geojson模块和@Types/Geojson类型解析TypeScrip中的GeoJSON?

类型TTextKey不能用于索引类型 ;TOption

笛卡尔产品类型

如何通过TypeScrip中的函数防止映射类型中未能通过复杂推理的any值

防止重复使用 Select 器重新渲染

API文件夹内的工作员getAuth()帮助器返回:{UserID:空}

为什么TypeScrip不能解析对象析构中的赋值?

ANGLE NumberValueAccessor表单控件值更改订阅两次

try 使Angular依赖注入工作

为什么TS2339;TS2339;TS2339:类型A上不存在属性a?

如何解决&Quot;类型不可分配给TypeScrip IF语句中的类型&Quot;?

基于属性值的条件类型

有没有一种方法可以防止我的组件包在其中安装样式组件'; node 模块中的s目录

TypeScript中的这些条件类型映射方法之间有什么区别?

剧作家测试未加载本地网址