TypeScript:assoc高阶函数嵌套属性路径类型解析异常求助
问题背景
正在学习函数式编程,尝试实现支持嵌套属性的assoc高阶函数(functional setter),通过函数重载限制嵌套深度,但使用三个及以上键时,TypeScript无法解析更深层级的键,出现类型错误。
当前实现代码
export type KeyOf<T> = T extends object ? keyof T : never; export type Setter<S, A> = (whole: S) => (part: A) => S; export function assoc<T, K1 extends KeyOf<T> = KeyOf<T>>( k1: K1 ): Setter<T, T[K1]>; export function assoc< T, K1 extends KeyOf<T> = KeyOf<T>, K2 extends KeyOf<T[K1]> = KeyOf<T[K1]> >(k1: K1, k2: K2): Setter<T, T[K1][K2]>; export function assoc< T, K1 extends KeyOf<T> = KeyOf<T>, K2 extends KeyOf<T[K1]> = KeyOf<T[K1]>, K3 extends KeyOf<T[K1][K2]> = KeyOf<T[K1][K2]> >(k1: K1, k2: K2, k3: K3): Setter<T, T[K1][K2][K3]>; export function assoc< T, K1 extends KeyOf<T> = KeyOf<T>, K2 extends KeyOf<T[K1]> = KeyOf<T[K1]>, K3 extends KeyOf<T[K1][K2]> = KeyOf<T[K1][K2]>, K4 extends KeyOf<T[K1][K2][K3]> = KeyOf<T[K1][K2][K3]> >(k1: K1, k2: K2, k3: K3, k4: K4): Setter<T, T[K1][K2][K3][K4]>; export function assoc(...path: any[]): any { return (whole: any) => { return (value: any) => { return assocDeep(whole, path as any, value); }; }; }
报错场景
当使用三层嵌套键时,编译器报错:
import { assoc } from './assoc'; type Company = { readonly id: number; readonly name: string; readonly address: Address; }; type Address = { readonly country: string; readonly region: string; readonly street: Street; readonly building: string; }; type Street = { readonly name: string; readonly kind: string; }; const setStreetName = assoc<Company>('address', 'street', 'name'); ~~~~~~
错误信息:
Argument of type 'string' is not assignable to parameter of type 'never'. ts(2345)
问题原因
原实现中,每个重载的类型参数都设置了默认值(如K1 extends KeyOf<T> = KeyOf<T>),当显式指定泛型参数<Company>时,TypeScript会优先使用默认值KeyOf<T>(即keyof Company的联合类型)。此时T[K1]会被推断为Company[keyof Company],也就是number | string | Address,而KeyOf<number | string | Address>结果为never(因为number和string不是对象类型),导致后续的K2、K3类型约束为never,传入具体键名时就会触发类型不匹配错误。
解决方案
方案1:移除类型参数的默认值
去掉各重载中类型参数的默认值,让TypeScript根据传入的实际键名逐步推断后续的类型约束:
export type KeyOf<T> = T extends object ? keyof T : never; export type Setter<S, A> = (whole: S) => (part: A) => S; export function assoc<T, K1 extends KeyOf<T>>( k1: K1 ): Setter<T, T[K1]>; export function assoc<T, K1 extends KeyOf<T>, K2 extends KeyOf<T[K1]>>( k1: K1, k2: K2 ): Setter<T, T[K1][K2]>; export function assoc<T, K1 extends KeyOf<T>, K2 extends KeyOf<T[K1]>, K3 extends KeyOf<T[K1][K2]>>( k1: K1, k2: K2, k3: K3 ): Setter<T, T[K1][K2][K3]>; export function assoc<T, K1 extends KeyOf<T>, K2 extends KeyOf<T[K1]>, K3 extends KeyOf<T[K1][K2]>, K4 extends KeyOf<T[K1][K2][K3]>>( k1: K1, k2: K2, k3: K3, k4: K4 ): Setter<T, T[K1][K2][K3][K4]>; export function assoc(...path: any[]): any { return (whole: any) => { return (value: any) => { return assocDeep(whole, path as any, value); }; }; }
修改后,调用assoc<Company>('address', 'street', 'name')时,TypeScript会先根据第一个参数'address'推断K1 = 'address',然后T[K1] = Address,进而推断K2 = 'street',T[K1][K2] = Street,最后K3 = 'name'完全符合类型约束,错误消失。
方案2:实现通用递归类型(支持任意深度嵌套)
如果需要支持任意层级的嵌套,无需手动编写多个重载,可以使用递归类型定义路径和对应的值类型:
export type Setter<S, A> = (whole: S) => (part: A) => S; // 递归定义元组形式的路径类型 type TuplePath<T> = T extends object ? { [K in keyof T]: [K] | [K, ...TuplePath<T[K]>]; }[keyof T] : []; // 根据路径获取对应的值类型 type PathValue<T, P extends TuplePath<T>> = P extends [infer K extends keyof T, ...infer Rest] ? Rest extends TuplePath<T[K]> ? PathValue<T[K], Rest> : never : T; // 实现通用assoc函数 export function assoc<T, P extends TuplePath<T>>( ...path: P ): Setter<T, PathValue<T, P>> { return (whole: T) => (value: PathValue<T, P>) => { // 替换为你原有的assocDeep实现,确保不可变性 return path.reduceRight((acc, key) => ({ ...acc, [key]: acc }), value) as T; }; }
这个方案无需限制嵌套深度,TypeScript会自动根据传入的路径推断对应的目标值类型,调用方式更灵活:
// 自动推断类型,无需显式指定<Company> const setStreetName = assoc('address', 'street', 'name'); // 类型为Setter<Company, string>
补充说明
如果保留原有的assocDeep实现,只需确保其逻辑正确处理深层对象的不可变性(如递归创建新对象而非修改原对象),即可配合上述类型定义实现类型安全的嵌套属性赋值。
内容的提问来源于stack exchange,提问作者Alex Chandler

