首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >映射对象类型,但属性类型不相同

映射对象类型,但属性类型不相同
EN

Stack Overflow用户
提问于 2022-05-20 13:58:42
回答 2查看 117关注 0票数 2

我想将结构类型转换为另一种结构类型。源结构是一个对象,它可能包含被点分割的属性键。我想把那些“逻辑组”扩展成子对象。

因此:

代码语言:javascript
复制
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;
    };
}

对此:

代码语言:javascript
复制
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;
    };
}

__

(编辑)以上这两种结构当然是基于一个真正的Firebase-Remote-Config类型转换,它不能有任意的道具(没有索引签名)或键冲突(我们不期望有任何logicGrouplogicGroup.anotherProperty)。

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

我们确实期望一个可选属性(logicGroup.timeout?: number)将维护相同的类型(logicGroup: { timeout?: number }),而不会成为强制性的,可以显式地接受undefined作为一个值(logicGroup: { timeout: number | undefined })。

我们期望所有的属性都是对象、字符串、数字、布尔值。没有数组,没有联合,没有交叉点。

我尝试过使用映射类型、键重命名、条件类型等等来进行精练。我想出了一个部分解决方案:

代码语言:javascript
复制
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;

它不能输出我想要的:

代码语言:javascript
复制
type X = UnwrapNested<MyInterface>;
//   ^?    ===> { logicGroup?: { serverString: string | undefined } | { testMode: boolean | undefined } ... }

因此,有两个问题:

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

因此,我试图阻止按服装K值进行分发:

代码语言:javascript
复制
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;

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

代码语言:javascript
复制
type X = UnwrapNested<MyInterface>;
//  ^?    ===> { logicGroup?: { serverString: string | boolean | number | undefined, ... }}

一个问题消失了,另一个问题又增加了。因此,问题是:

  1. 所有子属性都作为一种类型得到同一“逻辑组”中所有可用值的联合。
  2. 所有的子属性都保持| undefined,而不是实际上是可选的。

我还试着和其他选项一起玩,尝试过滤类型等等,但我不知道我到底错过了什么。

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

我不明白什么?非常感谢!

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2022-05-20 19:20:31

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

所有这些答案都肯定使用递归条件类型,其中Expand<T>是用对象定义的,对象的属性本身就是用Expand<T>编写的。

为了清晰起见,在进入Expand<T>之前,我将把定义分成几个助手类型。

首先,根据字符串中第一个点字符( 工会 )的位置和存在情况,筛选和转换字符串文字类型"."

代码语言:javascript
复制
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"

这些都是在模板文字类型中使用的推理。BeforeDot<T>T筛选为仅包含一个点的字符串,并计算出第一个点之前的字符串部分。AfterDot<T>是相似的,但是它的计算值是在第一个点之后的部分。NoDot<T>T过滤成没有点的字符串。

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

代码语言:javascript
复制
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>

代码语言:javascript
复制
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]>}的一部分。注意,我们使用as来过滤和转换键。因为我们在K in keyof T上迭代,而没有应用任何映射修饰符,所以这是一个保留输入属性修饰符的同态映射型。因此,对于键中没有点的任何属性,如果输入属性是可选的,那么输出属性将是可选的。readonly也是如此。

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

接下来,我们希望输出属性根本不是可选的。如果具有像"foo.bar"这样的键的属性是可选的,那么我们只希望最深的属性是可选的。有点像{foo: {bar?: any}},而不是{foo?: {bar: any}}{foo?: {bar?: any}}。至少这与问题中提出的ExpandedInterface一致。因此,我们使用-?映射修饰符。它负责处理密钥,并给我们{[K in keyof T as BeforeDot<K>]-?: ...}

至于带点的键的映射属性的值,我们需要在使用Expand递归之前对其进行转换。我们需要在不改变属性值类型的情况下,用第一个点之后的部分替换键K的合并。所以我们需要另一种映射类型。我们可以只编写{[P in K as AfterDot<P>]: T[P]},但是映射在T中不会是同态的,我们将失去任何可选的属性。为了确保这样的可选属性向下传播,我们需要使其同态和形式{[P in keyof XXXX]: ...},其中XXXX只有键在K中,但与T相同的修饰符。嘿,我们可以用实用程序类型。所以{[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>的定义:

代码语言:javascript
复制
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上测试一下

代码语言:javascript
复制
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

代码语言:javascript
复制
type MutuallyExtends<T extends U, U extends V, V = T> = void;

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

这样编译就没有错误,所以编译器认为ExpandMyInterfaceExpandedInterface本质上是一样的。万岁!

操场链接到代码

票数 2
EN

Stack Overflow用户

发布于 2022-05-20 14:37:14

我设法让它工作了,得到了类型fest的UnionToIntersection的帮助

代码语言:javascript
复制
import type { UnionToIntersection } from 'type-fest';

// Helper type for optional/undefined properties
type PartialOnUndefined<T extends object> = {
    [Key in {
        [K in keyof T]: undefined extends T[K] ? never : K
    }[keyof T]]: T[Key]
} & Partial<T>;

// Not strictly necessary, but makes the result more legible
type Expand<T> = T extends ReadonlyArray<unknown>
    ? number extends T["length"]
        ? Expand<T[number]>[]
        : { [K in keyof T]: Expand<T[K]> }
    : T extends object
        ? { [K in keyof T]: Expand<T[K]> }
        : T;

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

例如操场链接。

要非常清楚的是,我不明白UnwrapNested中巨大的三元结构是如何或为什么工作的。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72320170

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档