我正在开发用于合并对象的 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 }>>;
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 和 Disallow call with any 中所述,它利用了不健全性。 TypeScript 会将几乎所有 union 和 intersection 与
any
转换为 any
。 像 1 extends (0 & T)
这样的东西似乎总是错误的,因为据说 X extends (Y & Z)
需要 X extends Y
和 Y 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
错误分类为数组的问题,只需在继续之前返回 T[K]
(如果 U[K]
是 any
)即可。底部的检查会更改您的 U[K]
检查,因此它实际上会查找 any
(尽管我没有看到任何重要的示例)。
让我们测试您的代码:
type t11 = Expect<Equal<
MergeTypesWithPriority<{ a: Items }, { a: any }>,
{ a: Items }>
>; // okay
所有其他测试都通过了,现在这个也通过了。