我想将一种 struct 类型转换为另一种 struct 类型.源 struct 是一个对象,它可能包含由点分隔的属性键.我想将这些"逻辑组"扩展为子对象.

因此:

interface MyInterface {
    'logicGroup.timeout'?: number;
    'logicGroup.serverstring'?: string;
    'logicGroup.timeout2'?: number;
    'logicGroup.networkIdentifier'?: number;
    'logicGroup.clientProfile'?: string;
    'logicGroup.testMode'?: boolean;
    station?: string;
    other?: {
        "otherLG.a1": string;
        "otherLG.a2": number;
        "otherLG.a3": boolean;
        isAvailable: boolean;
    };
}

对此:

interface ExpandedInterface {
    logicGroup: {
        timeout?: number;
        serverstring?: string;
        timeout2?: number;
        networkIdentifier?: number;
        clientProfile?: string;
        testMode?: boolean;
    }
    station?: string;
    other?: {
        otherLG: {
            a1: string;
            a2: number;
            a3: boolean;
        },
        isAvailable: boolean;
    };
}

__

(EDIT)当然,上述两种 struct 基于真实的Firebase远程配置Typescript转置,不能有任意的props (没有索引签名)或键冲突(我们不希望有任何logicGrouplogicGroup.anotherProperty).

属性可以是可选的(logicGroup.timeout?: number),我们可以假设,如果logicGroup中的所有属性都是可选的,那么logicGroup本身也可以是可选的.

我们确实希望一个可选属性(logicGroup.timeout?: number)将保持相同的类型(logicGroup: { timeout?: number }),而不是成为强制属性,并可能显式接受undefined作为值(logicGroup: { timeout: number | undefined }).

我们希望所有属性都是对象、字符串、数字、布尔值.无数组、无并集、无交点.


我try 过使用映射类型、密钥重命名、条件类型等等.我想出了这个局部解决方案:

type UnwrapNested<T> = T extends object
    ? {
        [
            K in keyof T as K extends `${infer P}.${string}` ? P : K
        ]: K extends `${string}.${infer C}` ? UnwrapNested<Record<C, T[K]>> : UnwrapNested<T[K]>;
      }
    : T;

doesn't个输出我想要的:

type X = UnwrapNested<MyInterface>;
//   ^?    ===> { logicGroup?: { serverString: string | undefined } | { testMode: boolean | undefined } ... }

因此,有两个问题:

  1. logicGroup是一个分布式联合体
  2. logicGroup属性保留| undefined,而不是实际的可选属性.

因此,我试图通过服装K%的价值来阻止分销:

type UnwrapNested<T> = T extends object
    ? {
        [
            K in keyof T as K extends `${infer P}.${string}` ? P : K
        ]: [K] extends [`${string}.${infer C}`] ? UnwrapNested<Record<C, T[K]>> : UnwrapNested<T[K]>;
      }
    : T;

这是输出,但仍然不是我想要的:

type X = UnwrapNested<MyInterface>;
//  ^?    ===> { logicGroup?: { serverString: string | boolean | number | undefined, ... }}

一个问题消失,另一个问题出现.因此,问题是:

  1. 所有子属性都是同一"逻辑组"中所有可用值的a类并集
  2. 所有子属性都保留| undefined,而不是实际的可选属性.

我也try 过使用其他选项来过滤类型等等,但我不知道我缺少了什么.

我还找到了https://stackoverflow.com/a/50375286/2929433,它实际上是独立工作的,但我无法将它集成到我的公式中.

我不明白什么?

推荐答案

这种深度对象类型处理总是充满边缘情况.我将为Expand<T>提供一种可能的方法,它满足提问者的需求,但是遇到这个问题的任何其他人都应该小心地根据他们的用例测试所显示的任何解决方案.

所有这些答案都肯定使用recursive conditional types,其中Expand<T>是根据对象定义的,这些对象的属性本身是根据Expand<T>编写的.

为清楚起见,我将把定义分成一些助手类型,然后再继续到Expand<T>本身.


首先,我们要根据字符串中第一个点字符(".")的位置和存在情况过滤和转换字符串literal typesunions:

type BeforeDot<T extends PropertyKey> =
  T extends `${infer F}.${string}` ? F : never;
type AfterDot<T extends PropertyKey> =
  T extends `${string}.${infer R}` ? R : never;
type NoDot<T extends PropertyKey> =
  T extends `${string}.${string}` ? never : T;

type TestKeys = "abc.def" | "ghi.jkl" | "mno" | "pqr" | "stu.vwx.yz";
type TKBD = BeforeDot<TestKeys> // "abc" | "ghi" | "stu"
type TKAD = AfterDot<TestKeys> //  "def" | "jkl" | "vwx.yz"
type TKND = NoDot<TestKeys> // "mno" | "pqr"

这些都是在template literal type中使用推理.BeforeDot<T>T筛选为仅包含一个点的字符串,并计算为第一个点之前的字符串部分.AfterDot<T>类似,但其计算结果与第一个点之后的零件相同.和NoDot<T>过滤器T,仅将这些字符串without过滤为其中的一个点.


我们希望将对象类型合并到一个对象类型中;这基本上是一个intersection,但我们迭代交叉点的属性以将它们连接在一起:

type Merge<T, U> =
  (T & U) extends infer O ? { [K in keyof O]: O[K] } : never;

type TestMerge = Merge<{ a: 0, b?: 1 }, { c: 2, d?: 3 }>
// type TestMerge = { a: 0; b?: 1; c: 2; d?: 3 }

这主要是为了美学;我们可以只使用交叉点,但结果类型显示不太好.


现在是Expand<T>:

type Expand<T> = T extends object ? (
  Merge<
    {
      [K in keyof T as BeforeDot<K>]-?: Expand<
        { [P in keyof Pick<T, K> as AfterDot<P>]: T[P] }
      >
    }, {
      [K in keyof T as NoDot<K>]: Expand<T[K]>
    }
  >
) : T;

让我们分块判断这个定义...第一:T extends object ? (...) : T;如果T不是某种对象类型,我们根本不变换它.例如,我们希望Expand<string>正好是string.现在我们需要看看T是对象类型时会发生什么.

我们将此对象类型分为两部分:键包含点的属性和键不包含点的属性.对于没有点的键,我们只想对所有属性应用Expand.这是{[K in keyof T as NoDot<K>]: Expand<T[K]>}部分.请注意,我们使用key remapping via as来过滤和变换关键帧.因为我们在K in keyof T上迭代,而不应用任何mapping modifiers,所以这是一个homomorphic mapped type,它保留了输入属性的修饰符.因此,对于键中没有点的任何属性,如果输入属性是可选的,则输出属性将是可选的.与readonly相同.

对于属性键包含点的部分,我们需要做一些更复杂的事情.首先,我们需要将关键点转换为第一个点之前的部分.因此为[K in keyof T as BeforeDot<K>].请注意,如果两个键K具有相同的初始部分,如"foo.bar""foo.baz",则它们都将折叠为"foo",然后属性值所示的类型K将是并集"foo.bar" | "foo.baz".因此,我们需要在属性值中处理这样的联合键.

接下来,我们希望输出属性根本不是可选的.如果键为"foo.bar"的属性是可选的,那么我们只希望最深的属性是可选的.大约{foo: {bar?: any}},而不是{foo?: {bar: any}}{foo?: {bar?: any}}.至少这与问题中提出的ExpandedInterface相一致.因此,我们使用-?映射修改器.这会处理好 keys ,给我们{[K in keyof T as BeforeDot<K>]-?: ...}.

对于value个带有点的键的映射属性,我们需要在使用Expand递归之前对其进行转换.我们需要将键K的并集替换为第一个点的部分after,而不更改属性值类型.所以我们需要另一个映射类型.我们可以只写{[P in K as AfterDot<P>]: T[P]},但是映射在T中不会是同态的,我们会丢失任何可选属性.为了确保这些可选属性向下传播,我们需要使其同态,并采用{[P in keyof XXXX]: ...}的形式,其中XXXX只有K中的键,但修改器与T相同.嘿,我们可以用the Pick<T, K> utility type.所以{[P in keyof Pick<T, K> as AfterDot<P>]: T[P]}.我们Expand,所以Expand<{[P in keyof Pick<T, K> as AfterDot<P>]: T[P]}>.

好了,现在我们有了键上没有点的那块和键上有点的那块,我们需要把它们重新连接起来.这就是全部Expand<T>个定义:

type Expand<T> = T extends object ? (
  Merge<
    {
      [K in keyof T as BeforeDot<K>]-?: Expand<
        { [P in keyof Pick<T, K> as AfterDot<P>]: T[P] }
      >
    }, {
      [K in keyof T as NoDot<K>]: Expand<T[K]>
    }
  >
) : T;

好的,让我们在您的MyInterface上进行测试:

type ExpandMyInterface = Expand<MyInterface>;
/* type ExpandMyInterface = {
    logicGroup: {
        timeout?: number | undefined;
        serverstring?: string | undefined;
        timeout2?: number | undefined;
        networkIdentifier?: number | undefined;
        clientProfile?: string | undefined;
        testMode?: boolean | undefined;
    };
    station?: string | undefined;
    other?: {
        otherLG: {
            a1: string;
            a2: number;
            a3: boolean;
        };
        isAvailable: boolean;
    } | undefined;
} */

这看起来是一样的,但让我们确保编译器这样认为,通过名为MutuallyExtends<T, U>的助手类型,它只接受TU,这两个类型相互扩展:

type MutuallyExtends<T extends U, U extends V, V = T> = void;

type TestExpandedInterface = MutuallyExtends<ExpandedInterface, ExpandMyInterface> // okay

它编译时不会出错,因此编译器认为ExpandMyInterfaceExpandedInterface本质上是相同的.好极了

Playground link to code

Typescript相关问答推荐

为什么TypeScript失go 了类型推断,默认为any?''

为什么我的Set-Cookie在我执行下一次请求后被擦除

如何编写一个类型脚本函数,将一个对象映射(转换)为另一个对象并推断返回类型?

无法从Chrome清除还原的初始状态数据

是否使用非显式名称隔离在对象属性上声明的接口的内部类型?

声明文件中的类型继承

(TypeScript)TypeError:无法读取未定义的属性(正在读取映射)"

使用ngfor循环时,数据未绑定到表

如何扩展RxJS?

为什么TypeScrip假定{...省略类型,缺少类型,...选取类型,添加&>}为省略类型,缺少类型?

编剧- Select 动态下拉选项

为什么`map()`返回类型不能维护与输入相同数量的值?

更漂亮的格式数组<;T>;到T[]

为什么我会得到一个;并不是所有的代码路径都返回一个值0;switch 中的所有情况何时返回或抛出?(来自vsCode/TypeScript)

带动态键的 TS 功能

我应该使用服务方法(依赖于可观察的)在组件中进行计算吗?

为什么 Typescript 无法正确推断数组元素的类型?

可选通用映射器函数出错

为什么 TypeScript 类型条件会影响其分支的结果?

如何创建对象数组中特定键中使用的所有值的类型?