TypeScript类型合并

时间:2020-03-22 02:07:06

标签: typescript types typescript-typings typescript-generics

在某些情况下,我想“合并”默认类型联接(即T | UT & U)无法达到我想要的类型的

我要进行的是深度和智能类型合并,它将在合并期间自动将属性标记为可选,并执行TypeScript接口/类型的深度合并。

举个例子,假设我们有类型AB

type A = {
  a: string;
  b: number;
  c: boolean;
  d: {
    a2: string;
    b2: number;
  };
  e?: number;
};

type B = {
  a: string;
  b: boolean;
  d: {
    a2: string;
    c2: boolean;
  };
};

我正在寻找一个可以接受2个通用类型的Merge函数

type Merge<T, U> = ?????;

然后,如果在类型AB上使用,输出将如下所示

type AB = {
  a: string;
  b: number | boolean;
  c?: boolean;
  d: {
    a2: string;
    b2?: number;
    c2?: boolean;
  };
  e?: number;
};

如下所示,Merge类型将执行以下逻辑:

  1. 如果该属性同时存在于TU上并且是相同的类型,请根据需要对其进行标记,并在T / U中将其设置为该类型(就像属性a发生的情况一样。)
  2. 如果该属性同时存在于TU上,但类型不同,则将其标记为必需,如果它是原始类型,则将其设置为联合类型(如属性{{1 }}),或者如果它是一个对象则进行递归合并(如属性b发生的合并)。
  3. 如果该属性存在于一种类型而不是另一种类型,则将该属性标记为可选,并将其设置为它实际存在的输入类型中的类型(如属性d以及cb2)。
  4. 如果该属性在一种类型中已经是可选的,则在输出类型上该属性应该是可选的,并应用上述现有规则来确定其值(例如在属性c2中发生的情况)

假定您可以使用递归条件类型,尽管我知道它们为aren't yet officially supported,因此不应在生产中使用。我可以为生产用例制作类似于jcalz @的解决方案here的展开版本。

Here is a playground设置了要测试的问题。

1 个答案:

答案 0 :(得分:7)

TLDR:魔术!尝试Playground

所以,这是一个棘手的问题。不是因为合并要求,而是因为边缘情况。得到低垂的果实只需要不到20分钟的时间。确保它在任何地方都有效需要花费更多的时间...并使长度增加了两倍。工会很棘手!

  1. 什么是可选属性?在{ a: 1 | undefined, b?: 1 }中,a是可选属性吗?有人说是。其他没有就个人而言,我只在可选列表中包括b

  2. 您如何处理工会? Merge<{}, { a: 1} | { b: 2 }>的输出是什么?我认为最有意义的类型是{ a?: 1 } | { b?: 2 }Merge<string, { a: 1 }>呢?如果您根本不关心工会,那么这很容易...如果您这样做,则必须考虑所有这些。 (我在括号内选择的内容)

    1. Merge<never, never>never
    2. Merge<never, { a: 1 }>{ a?: 1 }
    3. Merge<string, { a: 1 }>string | { a?: 1 }
    4. Merge<string | { a: 1 }, { a: 2 }>string | { a: 1 | 2 }

让我们从助手开始找出这种类型。

当我想到工会这种类型的工会将变得复杂的时候,我就开始暗自嘲笑。 TypeScript没有很好的内置类型来测试类型相等性,但是我们可以编写一个助手类型,如果两种类型不相等,则会导致编译器错误。

(注意:Test类型可以进行改进,它可以允许传递不相等的类型,但是对于我们在这里的使用就足够了,同时保持非常简单)

type Pass = 'pass';
type Test<T, U> = [T] extends [U]
    ? [U] extends [T]
        ? Pass
        : { actual: T; expected: U }
    : { actual: T; expected: U };

function typeAssert<T extends Pass>() {}

我们可以像这样使用此助手:

// try changing Partial to Required
typeAssert<Test<Partial<{ a: 1 }>, { a?: 1 }>>();

接下来,我们将需要两种帮助程序类型。一种获取对象的所有必需键,另一种获取可选键。首先,进行一些测试以描述我们的追求:

typeAssert<Test<RequiredKeys<never>, never>>();
typeAssert<Test<RequiredKeys<{}>, never>>();
typeAssert<Test<RequiredKeys<{ a: 1; b: 1 | undefined }>, 'a' | 'b'>>();

typeAssert<Test<OptionalKeys<never>, never>>();
typeAssert<Test<OptionalKeys<{}>, never>>();
typeAssert<Test<OptionalKeys<{ a?: 1; b: 1, c: undefined }>, 'a'>>();

这里有两件事要注意。首先,*Keys<never>never。这很重要,因为稍后我们将在联合中使用这些帮助器,并且如果对象为never,则不应提供任何键。其次,这些测试都不包括联合检查。考虑到我所说的工会的重要性,这可能会让您感到惊讶。但是,这些类型仅在所有并集分发后使用,因此它们的行为无关紧要(尽管如果将这些包含在项目中,则可能要查看上述行为,可能会有所不同RequiredKeys(由于其写法)

这些类型通过给定的检查:

type OptionalKeys<T> = {
    [K in keyof T]-?: T extends Record<K, T[K]> ? never : K;
}[keyof T;

type RequiredKeys<T> = {
    [K in keyof T]-?: T extends Record<K, T[K]> ? K : never;
}[keyof T] & keyof T;

有关这些的注释:

  1. 使用-?删除属性的可选性,这使我们避免使用Exclude<..., undefined>的包装器
  2. T extends Record<K, T[K]>之所以有效,是因为{ a?: 1 }不会 扩展{ a: 1 | undefined }。我经历了几次迭代,然后才最终解决。您还可以检测其他映射类型为jcalz does here的可选性。
  3. 在版本3.8.3中,TypeScript可以正确推断出OptionalKeys的返回类型可分配给keyof T。但是,它无法为RequiredKeys检测到相同的内容。与keyof T相交可以解决此问题。

现在有了这些帮助程序,我们可以定义两种以上代表您的业务逻辑的类型。我们需要RequiredMergeKeys<T, U>OptionalMergeKeys<T, U>

type RequiredMergeKeys<T, U> = RequiredKeys<T> & RequiredKeys<U>;

type OptionalMergeKeys<T, U> =
    | OptionalKeys<T>
    | OptionalKeys<U>
    | Exclude<RequiredKeys<T>, RequiredKeys<U>>
    | Exclude<RequiredKeys<U>, RequiredKeys<T>>;

进行一些测试以确保它们的行为符合预期:

typeAssert<Test<OptionalMergeKeys<never, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<never, { a: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<never, { a?: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<{}, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<{ a: 1 }, { b: 2 }>, 'a' | 'b'>>();
typeAssert<Test<OptionalMergeKeys<{}, { a?: 1 }>, 'a'>>();

typeAssert<Test<RequiredMergeKeys<never, never>, never>>();
typeAssert<Test<RequiredMergeKeys<never, {}>, never>>();
typeAssert<Test<RequiredMergeKeys<never, { a: 1 }>, never>>();
typeAssert<Test<RequiredMergeKeys<{ a: 0 }, { a: 1 }>, 'a'>>();

现在有了这些,我们可以定义两个对象的合并,暂时不考虑基元和联合。这将调用我们尚未定义的顶级Merge类型来处理成员的基元和并集。

type MergeNonUnionObjects<T, U> = {
    [K in RequiredMergeKeys<T, U>]: Merge<T[K], U[K]>;
} & {
    [K in OptionalMergeKeys<T, U>]?: K extends keyof T
        ? K extends keyof U
            ? Merge<Exclude<T[K], undefined>, Exclude<U[K], undefined>>
            : T[K]
        : K extends keyof U
        ? U[K]
        : never;
};

(我在这里没有写特定的测试,因为我将其用于下一级别的考试)

我们需要同时处理联合和非对象。接下来让我们处理对象的并集。根据前面的讨论,我们需要分布在所有类型上并分别合并它们。这非常简单。

type MergeObjects<T, U> = [T] extends [never]
    ? U extends any
        ? MergeNonUnionObjects<T, U>
        : never
    : [U] extends [never]
    ? T extends any
        ? MergeNonUnionObjects<T, U>
        : never
    : T extends any
    ? U extends any
        ? MergeNonUnionObjects<T, U>
        : never
    : never;

请注意,我们对[T] extends [never][U] extends [never]进行了额外的检查。这是因为在分布子句中的never类似于for (let i = 0; i < 0; i++),它将永远不会输入条件的“主体”,因此将返回never,但是我们只希望never如果两个类型均为never

我们快到了!现在,我们可以处理合并对象,这是此问题中最难的部分。剩下的就是处理基元,我们可以通过形成所有可能的基元的联合,并将基元排除在传递给MergeObjects的类型之外来实现。

type Primitive = string | number | boolean | bigint | symbol | null | undefined;

type Merge<T, U> =
    | Extract<T | U, Primitive>
    | MergeObjects<Exclude<T, Primitive>, Exclude<U, Primitive>>;

有了这种类型,我们就完成了! Merge表现良好,只有50行左右没有注释。

关于生产类型的最后说明:

目前从Merge得到的类型是正确的,但它的可读性不尽如人意。现在,将鼠标悬停在结果类型上将显示交集和内部对象,并且内部包裹着Merge,而不显示结果。我们可以通过引入Expand类型来解决此问题,该类型强制TS将所有内容扩展为单个对象。

type Expand<T> = T extends Primitive ? T : { [K in keyof T]: T[K] };

现在只需修改MergeNonUnionObjects即可调用Expand。需要这样做的地方是反复试验的。您可以尝试是否包含它,以获得适合您的类型显示。

type MergeNonUnionObjects<T, U> = Expand<
    {
        [K in RequiredMergeKeys<T, U>]: Expand<Merge<T[K], U[K]>>;
    } & {
        [K in OptionalMergeKeys<T, U>]?: K extends keyof T
            ? K extends keyof U
                ? Expand<Merge<
                    Exclude<T[K], undefined>,
                    Exclude<U[K], undefined>
                >>
                : T[K]
            : K extends keyof U
            ? U[K]
            : never;
    }
>;

the playground中进行检查,其中包括我用来验证结果的所有测试。