网站搜索

TypeScript 工具类型 NestedKeyOf 在严格模式下使用点表示法失败


我正在使用一个自定义的 TypeScript 工具类型 NestedKeyOf 来将嵌套的键提取为点符号字符串,例如 "parent.child"。在 tsconfig.json 中将 "strict": false 时,该类型的表现如预期,但启用 "strict": true 会导致问题,尤其是对于可选属性。

这是我的 NestedKeyOf 类型定义:

    type NestedKeyOf<T> = T extends object
       ? {
            [K in keyof T & string]: T[K] extends object
              ? K | `${K}.${NestedKeyOf<T[K]>}`
              : K;
         }[keyof T & string]
       : never;

当在 tsConfig.json 的 angularCompilerOptions 中设置 "strict": false 时,这种类型在 Angular 编译期间按预期工作

示例代码:

这里有一个包含以下类型的示例:

export interface Node {
  name: string;
  version?: string;
  ip_address?: IPAddress;
}

export interface IPAddress {
  ipv4?: string;
  ipv6?: string;
};

我期望以下代码能够编译:

const nkn: NestedKeyOf<Node> = 'ip_address.ipv6';

链接到 StackBlitz

我已经创建了一个示例来重现这个问题。

答案#1

你遇到的问题是,当属性 T[K] 是像 IPAddress 这样的对象类型与像 undefined 这样的非对象类型的联合类型时,T[K] extends object 检查返回 false。启用了 --strictNullChecks 编译器选项(在使用 --strict 编译器功能套件时会自动启用)后,可选属性会自动将 undefined 添加到它们的类型中。因此 IPAddress | undefined 并不扩展 object,因此你的 NestedKeyOf 不会深入该类型。

你显然想说的是,如果你有一个像 IPAddress | undefined 这样的联合类型,你应该像处理单纯的 IPAddress 一样深入访问它(尽管 someNode.ip_address.ipv6 如果 ip_address 属性缺失可能会抛出运行时错误)。这里的一种可能方法是,如果一个属性的类型是 X | Y,你应该允许 X 的键,也应该允许 Y 的键。因此,如果你有 {prop: Node | IPAddress},你就可以允许 "prop.name" 和 "prop.ipv4"。当然,还有其他可能的方法,在你的例子中也能工作,但对边缘情况的处理有所不同。我认为这些不在问题所问的范围之内。

所以如果你希望 X | Y 将 X 和 Y 分开处理,并将结果放入一个联合类型中,那么你就想要将检查在 T[K] 中的联合类型上进行分配。而做到这一点最简单的方式是将其设为分配条件类型,通过给它自己的泛型工具类型(因为分配条件类型只会在你检查的类型是一个裸泛型类型参数时发生,比如 type Foo<T>=T extends ⋯,而不会像 type Foo<T>=T[K] extends ⋯ 那样更复杂):

type NestedKeyOf<T> = T extends object ? { [K in keyof T & string]:
  NestedKeyOfProp<T[K], K>;
}[keyof T & string] : never;

type NestedKeyOfProp<TK, K extends string> =
  TK extends object ? K | `${K}.${NestedKeyOf<TK>}` : K;

我基本上把这个支票拆分成了NestedKeyOfProp<T[K], K>,现在它在T[K]中分配了联合体。因此,NestedKeyOfProp<IPAddress | undefined, K> 等价于 NestedKeyOfProp<IPAddress, K> |NestedKeyOfProp<undefined, K>, is K | '$ {K}.$ {NestedKeyOf<T[K]>}' |K or just K | '$ {K}.$ {NestedKeyOf<TK>}'', as desired.

让我们测试它:

type NKN = NestedKeyOf<Node>;
//   ^? type NKN = "name" | "version" | 
//        "ip_address" | "ip_address.ipv4" | "ip_address.ipv6"

const nkn: NestedKeyOf<Node> = 'ip_address.ipv6'; // okay

看起来不错。

代码的 Playground 链接

答案#2

您需要将对象标记为所有键都是必需的。例如,如果 ip_address 是可选的,它的值是 IPAddress | undefined,这不是 object 的子集:

type T = Node['ip_address'] extends object ? true : false // false

所以你需要将所有对象键设置为必填,以便深入子对象:

操场

type NestedKeyOf<T extends object, R extends Required<T> = Required<T>> =  keyof {
    [K in keyof R & string as R[K] extends object ? K | `${K}.${NestedKeyOf<R[K]>}` : K]: any
}

另一种方法是手动处理可选属性:

操场

type NestedKeyOf<T extends object> =  keyof {
    [K in keyof T & string as T[K] extends undefined | object ? K | `${K}.${NestedKeyOf<Exclude<T[K], undefined>>}` : K]: any
}