TypeScript两种Partial Update类型等价性存疑:是类型差异还是测试缺陷?
TypeScript两种TaskUpdate类型的差异分析
问题背景
现有Task类型定义:
type Task = { id: string; prop1: string; prop2: number; }
需要定义任务更新参数类型,要求:
- id为必填项,用于定位任务
- 其余属性为可选项,代表要更新的任务属性
提出两种类型方案:
type TaskUpdate1 = Pick<Task, 'id'> & Partial<Omit<Task, 'id'>>; type TaskUpdate2 = Pick<Task, 'id'> & Partial<Task>;
直观上认为两种类型完全相同,但type-testing和spec.ts两个类型断言库却判定它们不同,测试代码如下(已内联测试库实现):
/* type-testing 核心实现 */ export type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; export type Expect<T extends true> = Equal<T, true>; /* spec.ts 核心实现 */ export class Any { private _: true = true; } type TestExact<Left, Right> = (<U>() => U extends Left ? 1 : 0) extends (<U>() => U extends Right ? 1 : 0) ? Any : never; type IsAny<T> = Any extends T ? ([T] extends [Any] ? 1 : 0) : 0; export type Test<Left, Right> = IsAny<Left> extends 1 ? IsAny<Right> extends 1 ? 1 : "❌ Left type is 'any' but right type is not" : IsAny<Right> extends 1 ? "❌ Right type is 'any' but left type is not" : [Left] extends [Right] ? [Right] extends [Left] ? Any extends TestExact<Left, Right> ? 1 : "❌ Unexpected or missing 'readonly' property" : "❌ Right type is not assignable to left type" : "❌ Left type is not assignable to right type"; type Assert<T, U> = U extends 1 ? T : IsAny<T> extends 1 ? never : U; export const assert: <Left, Right>( left: Assert<Left, Test<Left, Right>>, right: Assert<Right, Test<Left, Right>> ) => Right = () => ({}) as any; /* 业务代码 */ type Task = { id: string; prop1: string; prop2: number; } type TaskUpdate1 = Pick<Task, 'id'> & Partial<Omit<Task, 'id'>>; type TaskUpdate2 = Pick<Task, 'id'> & Partial<Task>; type test_0 = Expect<Equal<TaskUpdate1, TaskUpdate2>>; // 类型报错 var x = {} as TaskUpdate1; var y = {} as TaskUpdate2; assert(x, y); // 类型报错
核心结论
两种类型实际语义完全等价——不存在任何值能属于其中一种类型但不属于另一种。测试库的报错源于它们检测的是类型的结构表示细节,而非实际可赋值的语义范围。
详细分析
1. 两种类型的实际语义拆解
TaskUpdate1展开后是:{ id: string } & { prop1?: string; prop2?: number }TaskUpdate2展开后是:{ id: string } & { id?: string; prop1?: string; prop2?: number }
交叉类型中,同一属性的类型会取交集:id的类型为string & (string | undefined),最终等价于string(因为必填的string覆盖了可选的string | undefined)。因此,两种类型最终的属性约束完全一致:
id:必填stringprop1:可选stringprop2:可选number
任何符合TaskUpdate1的值必然符合TaskUpdate2,反之亦然。
2. 测试库判定不同的原因
测试库的Equal/TestExact类型采用高阶类型协逆变检测,会识别类型的内部结构差异:
TaskUpdate1的交叉部分不包含id的可选定义TaskUpdate2的交叉部分保留了id?: string的定义
虽然这两个结构最终的语义等价,但测试库会将这种结构细节的差异判定为类型不同。这种检测方式更关注类型的“定义形式”,而非“实际行为”。
总结
在业务代码中,两种类型可以互换使用,没有功能差异。测试库的报错只是因为它们对“类型等价”的定义更严格,关注了类型定义的结构细节而非实际可赋值范围。
内容的提问来源于stack exchange,提问作者Lesiak
相关产品推荐
相关产品推荐

