如何定义TypeScript中拆分对象为互补子对象的正确类型?
问题描述
在TypeScript中,如何定义通用类型,实现将一个对象拆分为两个包含互补键的子对象?比如把 {x: "x", y: "y", z:"z"} 这类对象拆成 {a: {x: "x"}, b: {y: "y", z:"z"}} 的结构。针对通用Record类型,该如何定义拆分后的类型?
我尝试了以下代码,但出现了错误:
type Split<Union extends Record<string, string | number>>= <A extends Partial<Union>>() => { a: A, b: Omit<Union, keyof A> }; type SpecificUnion = { x: string y: number z: string } const split: Split<SpecificUnion> = () => { return { a: { x: "x" }, b: { y: "y", z: "z"} }}
错误信息如下:
Type '() => { a: { x: string; }; b: { y: string; z: string; }; }' is not assignable to type 'Split<SpecificUnion>'. Call signature return types '{ a: { x: string; }; b: { y: string; z: string; }; }' and '{ a: A; b: Omit<SpecificUnion, keyof A>; }' are incompatible. The types of 'a' are incompatible between these types. Type '{ x: string; }' is not assignable to type 'A'. '{ x: string; }' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'Partial<SpecificUnion>'.
这个错误看起来矛盾:它既说 '{ x: string; }' 不能赋值给类型A,又说它符合A的约束条件。
错误原因分析
你当前的 Split 类型定义了一个泛型函数,泛型参数 A 的控制权在调用方,而非函数返回值。这意味着 Split<SpecificUnion> 要求函数能返回任意符合 Partial<SpecificUnion> 的 A 对应的结构,但你的函数只能返回固定的 a: {x: string} 和对应 b,类型自然不兼容。
简单说,你搞反了泛型的位置:应该让 Split 类型本身接收表示第一个子对象键集合的泛型参数,而非让函数成为泛型。
正确的类型定义方案
方案1:基于键集拆分的通用类型
定义泛型类型 Split<T, K extends keyof T>,其中 T 是原对象类型,K 是要放入 a 中的键集合:
type Split<T, K extends keyof T> = { a: Pick<T, K>; b: Omit<T, K>; };
使用时明确指定拆分的键:
type SpecificUnion = { x: string; y: number; z: string; }; // 将x拆分到a,剩余键放到b const splitResult: Split<SpecificUnion, "x"> = { a: { x: "x" }, b: { y: 123, z: "z" } // 注意:原代码中y赋值为字符串"y"不符合类型定义,需改为number类型 };
方案2:支持函数返回的泛型类型
如果需要用函数返回拆分后的对象,把泛型参数放到函数上:
type SplitFn<T> = <K extends keyof T>(a: Pick<T, K>, b: Omit<T, K>) => { a: Pick<T, K>, b: Omit<T, K> }; const split: SplitFn<SpecificUnion> = (a, b) => ({ a, b }); const result = split({ x: "x" }, { y: 456, z: "z" }); // result类型:{ a: Pick<SpecificUnion, "x">, b: Omit<SpecificUnion, "x"> }
更灵活的自动推导版本:
function split<T, K extends keyof T>(obj: T, keys: K[]): Split<T, K> { const a = {} as Pick<T, K>; const b = {} as Omit<T, K>; for (const key in obj) { if (keys.includes(key as K)) { a[key as K] = obj[key]; } else { b[key as Exclude<keyof T, K>] = obj[key]; } } return { a, b }; } const obj = { x: "x", y: 789, z: "z" }; const splitResult = split(obj, ["x"]); // splitResult.a: {x: string},splitResult.b: {y: number, z: string}
原代码报错的深层原因
Split<SpecificUnion> 是一个泛型函数类型,它要求函数能应对任意 A extends Partial<SpecificUnion> 的情况——比如调用时指定 A 为 {y: number},函数就需要返回 a: {y: number}, b: {x: string, z: string},但你的函数只能返回固定结构,因此TypeScript判定类型不兼容。
错误信息里的“可赋值给约束”是指 {x: string} 符合 Partial<SpecificUnion> 的约束,但“A可能被实例化为约束的不同子类型”(比如 {y: number}),此时函数返回的 a 不符合要求,所以报错。
内容的提问来源于stack exchange,提问作者Willem Vanhulle

