网站搜索

typescript - 如何正确合并两个对象类型?


我正在编写一个用于合并对象的 TypeScript 泛型,但我遇到了一个问题,测试用例 t11 失败了。

我想了解其背后的原因,以及如何修改 MergeTypesWithPriority 以使所有测试用例通过。

MergeTypesWithPriority 旨在优先考虑来自 T 的类型,并在类型为对象时具有递归逻辑。

type Expect<T extends true> = T
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

  type MergeTypesWithPriority<T, U> = {
    [K in keyof T | keyof U]: K extends keyof T
        ? K extends keyof U
            ? T[K] extends object
                ? U[K] extends object
                    ? T[K] extends any[]
                        ? T[K]
                        : U[K] extends any[]
                            ? U[K]
                            : MergeTypesWithPriority<T[K], U[K]>
                    : T[K]
                : T[K]
            : T[K]
        : K extends keyof U
            ? U[K] extends object
                ? T extends Record<K, any> 
                    ? U[K] extends any
                        ? T[K]
                        : U[K]
                    : U[K]
                : U[K]
            : never;
};


type t1 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: number }>, { a: string }>>;
type t2 = Expect<Equal<MergeTypesWithPriority<{ }, { a: number }>, { a: number }>>;
type t3 = Expect<Equal<MergeTypesWithPriority<{ a: number | undefined }, { a: number }>, { a: number | undefined }>>;
type t4 = Expect<Equal<MergeTypesWithPriority<{ a: { b: number } }, { a: { b: string } }>, { a: { b: number } }>>;
type t5 = Expect<Equal<MergeTypesWithPriority<{ a: { c: number } }, { a: { b: string } }>, { a: { b: string; c: number } }>>;
type t6 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: any }>, { a: string }>>;
type t7 = Expect<Equal<MergeTypesWithPriority<{ a?: number }, { a: any }>, { a: number | undefined }>>;

interface Items { foo: number; bar: { baz: number[] }}
type t8 = Expect<Equal<MergeTypesWithPriority<{ a: Items[] }, { a: any[] }>, { a: Items[] }>>;
type t9 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: Items }>, { a: string }>>;
type t10 = Expect<Equal<MergeTypesWithPriority<{ a: any }, { a: Items }>, { a: any }>>;
type t11 = Expect<Equal<MergeTypesWithPriority<{ a: Items }, { a: any }>, { a: Items }>>;

操场

答案#1

any 类型是故意不安全的。它既可以像 unknown 一样作为顶级类型,也可以像 never 类型一样(几乎)作为底层类型。any 可以赋值给其他所有类型,也可以从其他类型赋值(除了 any 不能赋值给 never)。这意味着如果被检查的类型是 any,代码中的许多条件类型行为将与您的预期不同。您的 U[K] extends object 检查如果 U[K] 是 any 时会 通过。U[K] extends any[] 检查也是如此。U[K] extends any 检查 总是 通过。

所以当 U 是 {a: any} 时,你的示例代码认为 a 属性是一个数组并返回它。哎呀。

如果你想以特殊方式处理 any,你需要更改你的检查。

我所知道的唯一可靠的方法是如《Typescript check for the 'any' type and Disallow call with any》中所描述的,它利用了不安全性。TypeScript 几乎会将每个包含 any 的联合类型和交叉类型都转换为 any。像 1 extends (0 & T) 这样的表达式似乎总是为假,因为据说 X extends (Y & Z) 需要同时满足 X extends Y 和 X extends Z。由于 1 extends 0 为假,这个表达式也应该总是为假。但是如果 T 是 any,那么 0 & T 也会是 any(不安全、不可靠,但这是预期的),然后 1 extends any 就是 true。所以你可以利用这种不安全性来检测 any。

我将重写你代码中的一些相关部分:

type MergeTypesWithPriority<T, U> = 1 extends (T & U & 0) ? T : { // <--
    [K in keyof T | keyof U]: K extends keyof T
        ? K extends keyof U
            ? T[K] extends object
                ? U[K] extends object
                    ? T[K] extends any[]
                        ? T[K]
                        : U[K] extends any[]
                            ? 1 extends (U[K] & 0) ? T[K] : U[K] // <--
                            : MergeTypesWithPriority<T[K], U[K]>
                    : T[K]
                : T[K]
            : T[K]
        : K extends keyof U
            ? U[K] extends object
                ? T extends Record<K, any> 
                    ? 1 extends (U[K] & 0) // <--
                        ? T[K]
                        : U[K]
                    : U[K]
                : U[K]
            : never;
};

顶部的检查如果 T 或 U 为 any 就会提前退出,并返回 T。中间的检查解决了你将 any 错误分类为数组的问题,通过在继续之前,如果 U[K] 是 any,直接返回 T[K]。底部的检查更改了你的 U[K] 检查,使其实际查找 any(虽然我没有看到有任何示例显示这有影响)。

让我们测试你的代码:

type t11 = Expect<Equal<
  MergeTypesWithPriority<{ a: Items }, { a: any }>, 
  { a: Items }>
>; // okay

你所有的其他测试都通过了,现在这个测试也通过了。

代码的 Playground 链接