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 链接
