递归 PartialAllExcept

Recursive PartialAllExcept

我正在尝试创建一个类型别名,它将递归定义具有以下条件的类型:

  1. 删除id属性
  2. 使所有属性部分化
  3. 需要属性
  4. 如果 属性 类型也有 id,删除它并使所有属性部分化(除了 name 如果它存在)

供参考,这是代码:

type IDType = string | number;

type Person = { id: IDType };

type PartialAllExcept<
  T, // extends Person,
  R extends keyof T | string = 'name'
> = Partial<Omit<T, 'id' | R>> & R extends keyof T
  ? {
      [K in R]: R extends keyof T
        ? T[R] extends Person
          ? PartialAllExcept<T[R]>
          : T[R]
        : any;
    }
  : {};

type Parent = Person & {
  name: string;
  single?: boolean;
};

type Child = Person & {
  parent?: Parent | IDType;
  name: string;
  adult: boolean;
};

const data: PartialAllExcept<Child, 'parent'> = {
  name: 'John Doe',
  parent: { name: 'John Doe Sr' },
  adult: true,
};

尽管条件如此,即使我要对所需的内容进行硬编码 属性,也就是说做

type PartialAllExcept<
  T, // extends Person,
  R extends keyof T | string = 'name'
> = Partial<Omit<T, 'id' | R>> & R extends keyof T
  ? {
      [K in R]: { name: string };
    }
  : {};

那仍然给我 Property 'id' is missing in type '{ name: string; }' but required in type 'Person'

关于我目前调查的内容的更多说明:

  1. 因为 Child["parent"] 可以是 Parent 以外的其他值(扩展 Person)- IDType | undefined,只要保持 Parent 就可以解决这个问题,但我的应用程序要求它在某些地方是 IDType | undefined。除此之外,我还收到另一个错误 - Object literal may only specify known properties, and 'name' does not exist in type '{ parent: { name: string; }; }'(更多内容在以下几点)
  2. 通用 T 应该扩展 Person,但现在被注释掉了 - 不是一个破坏因素,但如果包含在内,那么递归调用会给出 - Type 'T[string]' is not assignable to type 'Person'.
  3. 因为它也是一个递归调用,我们不知道 属性 扩展 Person 需要哪些属性(以及其他部分属性),默认的 "name" 是给出,但是这将给出 - Type 'string' does not satisfy the constraint 'keyof T'。为了抑制这种情况,R 已扩展 string,而理想情况下它应该只是 keyof T。这很好,但是,当 R 的值不是 keyof T 时(比如 name 属性 在 T 上不存在),那么不应添加该必填字段 - 因此末尾为空 {}

它没有在您的要求列表中明确提及,但如果一个对象具有 属性 或 union type like Parent | string, you want that union to be broken into its constituent elements, transformed individually, and then reassembled into a new union. That is, you want the operation to distribute over unions. But your version of the conditional type,那么 属性 映射代码 不会 分配给工会:

T[R] extends Person ? PartialAllExcept<T[R]> : T[R]

如果 T[R]Parent | string,则编译器将此类型评估为单个聚合单元。由于 Parent | string 不扩展 Person(如果你有一个 Parent | string 类型的值,你不能安全地将它分配给 Person 类型的变量),这个条件类型评估只是 T[R],也就是 Parent | string。因此,Parent 部分没有具有可选属性,删除了 id 等,而是保持 as-is 并且您会收到错误消息。

这就是它不起作用的主要原因。


如果您有条件类型并且希望它分布在联合体中,则需要将其变成 distributive conditional type。为了实现这一点,您正在检查的类型(A extends B ? C : D 中的 A 类型)需要是单个 类型参数 。由于 T[R] 不是类型参数,因此它不是分布式的。 (T是类型参数,R是类型参数,但T[R]是类型参数的组合。)

解决这个问题的一个简单方法是将有问题的条件类型重构为它自己的通用类型,其中检查的类型是普通类型参数:

type Refactored<X, R extends string> = 
  X extends Person ? PartialAllExcept<X, R> : X;

然后使用它:

Refactored<T[R]>

现在 Refactored<Parent | string> 将分发到 Parent | string。你会得到 (Parent extends Person ? PartialAllExcept<Parent, R> : Parent) | (string extends Person ? PartialAllExcept<string, R> : string) ,它根据需要评估为 PartialAllExcept<Parent, R> | string


当我解决这个问题(以及其他一些小问题)时,我得到以下类型:

type PartialAllExcept<T extends Person, R extends string = 'name'> =
  Partial<Omit<T, 'id' | R>> & (R extends keyof T ?
    { [K in R]: MaybePartialAllExcept<T[K], R> } : unknown
  );

type MaybePartialAllExcept<T, R extends string> =
  T extends Person ? PartialAllExcept<T, R> : T;

现在一切都按照您想要的方式进行:

const data: PartialAllExcept<Child, 'parent'> = {
  name: 'John Doe',
  parent: { name: 'John Doe Sr' }, // okay
  adult: true,
};

Playground link to code