TypeScript中Pick<Foo, Bar> & Omit<Foo, Bar>为何不等于Foo?
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

