如何定义类型并确保它是打字稿中另一种类型的一部分?

How to define a type and make sure it's part of another type in typescript?

我有一个大类型:

type BigType = {
  aaa: string,
  bbb?: number,
  ccc: boolean[],
  extra?: {
    [key in string]?: string
  },
  nested1: {
    nested2: {
      nested3: {
        [key in string]?: string
      }
    }
  }
}

我想定义另一种类型并确保它是 BigType 的子集,所以我定义了一个 RecursivePartial 类型:

type RecursivePartial<T> = {
  [P in keyof T]?:
  T[P] extends (infer U)[] ? RecursivePartial<U>[] :
    T[P] extends object ? RecursivePartial<T[P]> :
      T[P];
};

type PartOf<T, X extends RecursivePartial<T>> = X;

现在我可以定义一个小类型,它只是 BigType 的一部分:

type SmallType = PartOf<BigType, {
  aaa: string;
  extra: { ddd: string };
  nested1: { nested2: {} }
}>

问题是我也可以添加不属于 BigType 的属性:

type SmallType = PartOf<BigType, {
  aaa: string;
  extra: { ddd: string };
  nested1: { nested2: {} },

  someOtherProperty1: string, // not part of BigType
  someOtherProperty2: string, // not part of BigType
}>

如何解决?

这里的问题是 TypeScript 中的对象类型是开放的而不是 exact (see #12936 for discussion about exact types). That is, you can object types A and B where A extends B and A has properties that B doesn't mention. This is actually a crucial part of interface/class hierarchies; without it, you couldn't add properties to subinterfaces/subclasses. Still, there are times when it surprises people (especially because when you use object literal values the compiler performs additional excess property checking 这使得它看起来对象类型是准确的)。

目前无法在 TypeScript 中将确切的对象类型表示为特定的具体类型。相反,您必须使用泛型(有关详细信息,请参阅 this GitHub comment


无论如何,在你的情况下,我可能会通过定义 DeepPartialDeepNoExcess 类型别名并在 TypeOf 中使用它们来继续。 DeepPartial 看起来像这样:

type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };

我认为这与您的 RecursivePartial 基本相同。从 TS3.1 开始,mapped types automatically map over arrays and tuples without needing special casing, and recursive mapped types that encounter primitive types leave them unmapped (see microsoft/TypeScript#12447)。这意味着您不需要做很多事情来获得递归 Partial.

DeepNoExcess 必须同时采用主要类型和候选类型(因为无法具体表示确切的类型):

type DeepNoExcess<T, U> = { [K in keyof U]:
    K extends keyof T ? DeepNoExcess<Required<T>[K], U[K]> :
    never };

这将遍历候选类型 U 的属性,如果 属性 键不存在于 T。我不得不走进 Required<T>,而不仅仅是 T,因为您的可选属性没有得到正确处理(keyof (SomeType | undefined) 往往是 never)。

那么PartOf就是这样定义的:

type PartOf<T, U extends DeepPartial<T> & DeepNoExcess<T, U>> = U;

这会产生您所希望的两个示例的行为:

type GoodSmallType = PartOf<BigType, {
    aaa: string;
    extra: { ddd: string };
    nested1: { nested2: {} }
}>; // okay

type BadSmallType = PartOf<BigType, {
    aaa: string;
    extra: { ddd: string };
    nested1: { nested2: {} },

    someOtherProperty1: string, // not part of BigType
    someOtherProperty2: string, // not part of BigType
}>; // error! Types of property 'someOtherProperty1' are incompatible.

是否满足您所有的用例尚不清楚;您可以做出很多决定(例如 Required<T> 而不是 T),这些决定会影响接受哪些类型以及不接受哪些类型。但希望这至少能给你一条前进的道路。祝你好运!

Playground link to code