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

TypeScript中Pick<Foo, Bar> & Omit<Foo, Bar>为何不等于Foo?

TypeScript 中 Pick<Foo, Bar> & Omit<Foo, Bar> 不等于 Foo 的类型错误解析

问题描述

你在编写泛型绑定函数时遇到了类型错误,代码如下:

export function bind<
  TOpts extends Record<string, unknown>,
  TReturn,
  TBound extends keyof TOpts
>(
  fn: (opts: TOpts) => TReturn,
  bound: Pick<TOpts, TBound>
): (opts: Omit<TOpts, TBound>) => TReturn {
  return (opts) => {
    return fn({
      ...bound,
      ...opts,
    });
  };
}

错误提示:

Argument of type 'Pick<TOpts, TBound> & Omit<TOpts, TBound>' is not assignable to parameter of type 'TOpts'. 'Pick<TOpts, TBound> & Omit<TOpts, TBound>' is assignable to the constraint of type 'TOpts', but 'TOpts' could be instantiated with a different subtype of constraint 'Record<string, unknown>'.

你原本认为Pick<TOpts, TBound>选择TOpts中TBound对应的键,Omit<TOpts, TBound>选择剩余键,两者交叉类型应完全等于TOpts,但实际触发了类型错误。

错误合理性判断

这个错误是完全合理的,并非TypeScript优化导致的异常。TypeScript基于结构子类型系统,但泛型参数TOpts可被实例化为Record<string, unknown>的任意子类型,Pick<TOpts, TBound> & Omit<TOpts, TBound>仅能保证结构上包含TOpts的所有属性,无法匹配TOpts子类型的额外约束。

触发不相等的具体场景

以下是几种典型的Pick<TOpts, TBound> & Omit<TOpts, TBound> !== TOpts的情况:

1. TOpts 是类实例类型

类实例类型不仅包含公开属性,还可能包含私有/受保护属性、原型方法等,而Pick和Omit仅处理公开属性键。合并后的普通对象无法满足类实例的类型约束:

class MyOpts {
  private secret: string;
  public value: number;
  constructor(value: number) {
    this.value = value;
    this.secret = "internal";
  }
}

// TOpts 被实例化为 MyOpts
const getValue = (opts: MyOpts) => opts.value;
const boundValue = bind(getValue, { value: 10 });

// 合并后的 { value:10 } 不是 MyOpts 类实例,缺少私有属性,无法赋值给 MyOpts 类型

2. TOpts 包含只读属性

Pick会保留属性的只读修饰符,但通过Spread操作创建的合并对象属性是可写的,与原类型的只读约束冲突:

interface ReadonlyConfig {
  readonly id: string;
  name: string;
}

const printConfig = (opts: ReadonlyConfig) => console.log(opts.id);
const boundConfig = bind(printConfig, { id: "cfg-001" });

// 合并后的对象 id 属性可写,无法匹配要求 readonly 的 ReadonlyConfig 类型

3. TOpts 是精确字面量类型

精确字面量类型(如{ a: 1, b: 2 })的Pick和Omit交叉类型会被放宽为更宽泛的类型(如{ a: number, b: number }),无法匹配原字面量的精确值约束:

type ExactOpts = { a: 1; b: 2 };
const sum = (opts: ExactOpts) => opts.a + opts.b;
const boundSum = bind(sum, { a: 1 });

// 合并后的对象无法保证 b 值严格等于 2,无法匹配 ExactOpts 类型

4. TOpts 包含索引签名与额外约束

带有索引签名且附加属性约束的类型,交叉类型可能违反原类型的约束:

type StrictIndexOpts = Record<string, never> & { key: string };
// 该类型仅允许存在 key 属性,不允许其他属性

const strictFn = (opts: StrictIndexOpts) => opts.key;
const boundStrict = bind(strictFn, { key: "test" });

// 合并后的对象可能被推断为带有额外属性的类型,违反 Record<string, never> 的约束

结论

TypeScript的错误提示合理,泛型参数的具体化存在不确定性,交叉类型无法覆盖所有子类型的额外约束。若要消除错误,可使用类型断言fn({ ...bound, ...opts } as TOpts),但需确保业务逻辑不会出现上述不兼容场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 11:25:28