You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:必填string
  • prop1:可选string
  • prop2:可选number

任何符合TaskUpdate1的值必然符合TaskUpdate2,反之亦然。

2. 测试库判定不同的原因

测试库的Equal/TestExact类型采用高阶类型协逆变检测,会识别类型的内部结构差异:

  • TaskUpdate1的交叉部分不包含id的可选定义
  • TaskUpdate2的交叉部分保留了id?: string的定义

虽然这两个结构最终的语义等价,但测试库会将这种结构细节的差异判定为类型不同。这种检测方式更关注类型的“定义形式”,而非“实际行为”。

总结

在业务代码中,两种类型可以互换使用,没有功能差异。测试库的报错只是因为它们对“类型等价”的定义更严格,关注了类型定义的结构细节而非实际可赋值范围。

内容的提问来源于stack exchange,提问作者Lesiak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 21:41:02