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

TypeScript:assoc高阶函数嵌套属性路径类型解析异常求助

实现支持深层嵌套的assoc函数(Functional Setter)

问题背景

正在学习函数式编程,尝试实现支持嵌套属性的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 18:45:42