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

TypeScript嵌套映射问题:预期对象类型却推断为never

问题:嵌套对象类型被推断为never而非预期类型

开发TypeScript映射函数时,遇到嵌套对象类型推断错误的问题:处理后的processedResult.building类型被推断为never,但预期应为Building类型。函数支持简单字段映射、嵌套对象映射及验证逻辑,问题出在MapResult类型定义的嵌套推断逻辑上。

以下是简化后的代码:

type SimpleFieldOptions = {
  inputKey?: string;
  validators?: Array<(value: any) => boolean>;
};

type NestedFieldOptions<M> = {
  nested: FieldMapping<M>;
};

type FieldMapping<M> = {
  [K in keyof M]: SimpleFieldOptions | NestedFieldOptions<M[K]> | true;
};

type DataMapping<M> = {
  fields: FieldMapping<M>;
};

type MapResult<M, F extends DataMapping<M>> = {
  [K in keyof F['fields']]: F['fields'][K] extends SimpleFieldOptions
    ? M[K]
    : F['fields'][K] extends NestedFieldOptions<infer N>
    ? MapResult<N, DataMapping<N>>
    : F['fields'][K] extends true
    ? M[K]
    : never;
};

function mapData<M, F extends DataMapping<M>>(payload: Record<string, any>, mapping: F): MapResult<M, F> {
  const result = {} as MapResult<M, F>;

  for (const key in mapping.fields) {
    const fieldConfig = mapping.fields[key];
    const typedKey = key as keyof MapResult<M, F>;

    if (fieldConfig === true) {
      result[typedKey] = payload[key] as M[typeof typedKey];
    } else if (isSimpleFieldOptions(fieldConfig)) {
      const value = payload[fieldConfig.inputKey || key];

      if (fieldConfig.validators) {
        for (const validator of fieldConfig.validators) {
          if (!validator(value)) {
            throw new Error(`Validation failed for key: ${String(typedKey)} with value: ${value}`);
          }
        }
      }
      result[typedKey] = value as M[typeof typedKey];
    } else if (isNestedFieldOptions(fieldConfig)) {
      result[typedKey] = mapData(payload, { fields: fieldConfig.nested }) as any;
    }
  }

  return result;
}

function isSimpleFieldOptions(option: any): option is SimpleFieldOptions {
  return option && typeof option === 'object' && ('inputKey' in option || 'validators' in option);
}

function isNestedFieldOptions<T>(option: any): option is NestedFieldOptions<T> {
  return option && typeof option === 'object' && option.nested && typeof option.nested === 'object';
}

class Address {
  street!: string;
  city!: string;
  zipCode!: string;
}

class Building {
  name!: string;
  description!: string;
  address!: Address;
}

class Apartment {
  apartmentId!: string;
  buildingId!: string;
  building!: Building;
}

const apartmentMapping: DataMapping<Apartment> = {
  fields: {
    apartmentId: true,
    buildingId: true,
    building: {
      nested: {
        name: { inputKey: 'buildingName', validators: [(value) => typeof value === 'string'] },
        description: { inputKey: 'buildingDescription', validators: [(value) => typeof value === 'string'] },
        address: {
          nested: {
            street: { inputKey: 'streetName', validators: [(value) => typeof value === 'string'] },
            city: { inputKey: 'cityName', validators: [(value) => typeof value === 'string'] },
            zipCode: true,
          },
        },
      },
    },
  },
};

const payload = {
  apartmentId: '321-cba',
  buildingId: '123-abc',
  buildingName: 'Sunrise Apartments',
  buildingDescription: 'A modern apartment complex.',
  streetName: 'Sunset Boulevard',
  cityName: 'Los Angeles',
  zipCode: '90001',
};

const processedResult = mapData(payload, apartmentMapping);

// 预期 processedResult.building 类型为 Building,但实际为 never

解决方案

问题核心在于MapResult类型的嵌套推断逻辑错误:原定义中通过infer N提取的是FieldMapping类型而非目标实体类型(如Building),导致递归推断时类型失配。

步骤1:修正MapResult类型定义

调整嵌套字段的类型推断逻辑,直接从原始目标类型M中获取对应嵌套类型,而非从映射配置中推断:

type MapResult<M, F extends DataMapping<M>> = {
  [K in keyof F['fields']]: 
    // 简单字段或直接映射,返回M[K]类型
    F['fields'][K] extends SimpleFieldOptions | true 
      ? M[K] 
      : // 嵌套字段,递归处理M[K]对应的映射配置
        F['fields'][K] extends NestedFieldOptions<any>
        ? MapResult<M[K], { fields: F['fields'][K]['nested'] }>
        : never;
};

步骤2:优化函数内的类型断言

在嵌套调用mapData时,添加更精确的类型断言,避免使用any:

// 替换原嵌套分支的代码
else if (isNestedFieldOptions(fieldConfig)) {
  result[typedKey] = mapData(payload, { fields: fieldConfig.nested }) as MapResult<M[typeof typedKey], { fields: typeof fieldConfig.nested }>;
}

修正后效果

修改后,processedResult.building的类型将正确推断为Building,其嵌套的address字段也会正确推断为Address类型,完全符合预期。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 17:33:25