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

TypeScript:解决Type '"id"'无法索引类型及addThing编译错误

问题描述

addThingExample2 与 addThing 定义逻辑相似,区别在于前者直接引用联合类型 Things,后者使用泛型参数 THING extends Thing。

PropsMapper<Things>[TYPE]['id'] 可正常编译无类型错误,但 PropsMapper<THING>[TYPE]['id'] 会报错:Type '"id"' cannot be used to index type 'PropsMapper<THING>[TYPE]'。需要修改 addThing 函数,使其编译通过且满足参数间的类型约束要求。

原始代码如下:

type AllowedThingType = 'Smartphone' | 'Toy' | 'Magazine';

type ThingType<TYPE extends AllowedThingType, PROPS extends Record<string, unknown>> = {type: TYPE, props: PROPS};

type ThingTypes = 
  | ThingType<'Smartphone', {color: 'blue' | 'red', price: number}> 
  | ThingType<'Toy', {size: 'small' | 'medium'}>
  | ThingType<'Magazine', {theme: 'nature' | 'games'}>;

type Thing<TYPE extends ThingTypes['type'] = ThingTypes['type'], ID extends string = string> = {id: ID, type: TYPE };

type Things = Thing<'Smartphone', 'Samsung S21'> | Thing<'Smartphone', 'Google Pixel 7'> | Thing<'Toy', 'Sheriff Woody'>;

// 示例1 - 无约束
function addThingExample1(type: Things['type'], id: Things['id'], props: ThingTypes['props']) { /* ... */ }
// 以下代码可正常编译,但不符合需求,因为它未约束参数间的关联性...
addThingExample1('Smartphone', 'Sheriff Woody', { theme: 'nature' });

// 示例2 - 带约束
type TypeProps = { [K in ThingTypes as K['type']]: { props: K['props'] }};
type PropsMapper<T extends Thing> = { [K in T as K['type']]: {id: K['id'], props: TypeProps[K['type']]['props'] }};

// 以下代码按预期工作,TypeScript会确保传入的type、id和props组合一致:
function addThingExample2<TYPE extends keyof PropsMapper<Things>>(type: TYPE, id: PropsMapper<Things>[TYPE]['id'], props: PropsMapper<Things>[TYPE]['props']) { /* ... */ }
addThingExample2('Smartphone', 'Sheriff Woody', { theme: 'nature' }); // 触发类型错误:参数"Sheriff Woody"无法赋值给类型"Samsung S21" | "Google Pixel 7"
addThingExample2('Smartphone', 'Google Pixel 7', { color: 'red', price: 10 }); // 无类型错误

// 示例3 - 动态Thing
class ThingsRepository<THING extends Thing> {

  addThing<TYPE extends keyof PropsMapper<THING>>(type: TYPE, id: PropsMapper<THING>[TYPE]['id'], props: PropsMapper<THING>[TYPE]['props']) {
    // 此函数签名与addThingExample2类似,但使用类泛型参数THING而非直接引用Things类型... 
    // 如何让它正常工作?
  }
}

// 期望的使用方式:
type MyThings =  Thing<'Smartphone', 'Samsung S21'> | Thing<'Smartphone', 'Google Pixel 7'> | Thing<'Toy', 'Sheriff Woody'>;
const myThingsRepo = new ThingsRepository<MyThings>();
myThingsRepo.addThing('Smartphone', 'Sheriff Woody', { theme: 'nature' }); // 此处应触发类型错误...
myThingsRepo.addThing('Smartphone', 'Google Pixel 7', {color: 'blue', price: 10}); // 无类型错误...
解决方案

核心问题在于TypeScript无法推断泛型参数THING对应的PropsMapper结构中,每个类型键下必然包含id和props字段。通过重构类型定义和调整函数签名,可以解决该问题。

步骤1:重构PropsMapper类型

使用Extract工具类型精准提取对应类型的id,替代原有的映射方式,让TypeScript明确每个类型键对应的结构:

type PropsMapper<T extends Thing> = {
  [K in T['type']]: {
    id: Extract<T, { type: K }>['id'];
    props: TypeProps[K]['props'];
  };
};

步骤2:调整addThing函数签名

将TYPE的约束改为THING['type'],直接关联类型与对应的id、props结构:

class ThingsRepository<THING extends Thing> {
  addThing<TYPE extends THING['type']>(
    type: TYPE,
    id: PropsMapper<THING>[TYPE]['id'],
    props: PropsMapper<THING>[TYPE]['props']
  ) {
    // 函数逻辑
  }
}

完整修正代码

type AllowedThingType = 'Smartphone' | 'Toy' | 'Magazine';

type ThingType<TYPE extends AllowedThingType, PROPS extends Record<string, unknown>> = { type: TYPE, props: PROPS };

type ThingTypes = 
  | ThingType<'Smartphone', { color: 'blue' | 'red', price: number }> 
  | ThingType<'Toy', { size: 'small' | 'medium' }>
  | ThingType<'Magazine', { theme: 'nature' | 'games' }>;

type Thing<TYPE extends ThingTypes['type'] = ThingTypes['type'], ID extends string = string> = { id: ID, type: TYPE };

type Things = Thing<'Smartphone', 'Samsung S21'> | Thing<'Smartphone', 'Google Pixel 7'> | Thing<'Toy', 'Sheriff Woody'>;

// 示例1 - 无约束
function addThingExample1(type: Things['type'], id: Things['id'], props: ThingTypes['props']) { /* ... */ }
addThingExample1('Smartphone', 'Sheriff Woody', { theme: 'nature' });

// 示例2 - 带约束
type TypeProps = { [K in ThingTypes as K['type']]: { props: K['props'] } };
type PropsMapper<T extends Thing> = {
  [K in T['type']]: {
    id: Extract<T, { type: K }>['id'];
    props: TypeProps[K]['props'];
  };
};

function addThingExample2<TYPE extends keyof PropsMapper<Things>>(type: TYPE, id: PropsMapper<Things>[TYPE]['id'], props: PropsMapper<Things>[TYPE]['props']) { /* ... */ }
addThingExample2('Smartphone', 'Sheriff Woody', { theme: 'nature' }); // 触发类型错误,符合预期
addThingExample2('Smartphone', 'Google Pixel 7', { color: 'red', price: 10 }); // 无类型错误,符合预期

// 示例3 - 动态Thing
class ThingsRepository<THING extends Thing> {
  addThing<TYPE extends THING['type']>(
    type: TYPE,
    id: PropsMapper<THING>[TYPE]['id'],
    props: PropsMapper<THING>[TYPE]['props']
  ) {
    // 函数逻辑
  }
}

// 期望的使用方式:
type MyThings = Thing<'Smartphone', 'Samsung S21'> | Thing<'Smartphone', 'Google Pixel 7'> | Thing<'Toy', 'Sheriff Woody'>;
const myThingsRepo = new ThingsRepository<MyThings>();
myThingsRepo.addThing('Smartphone', 'Sheriff Woody', { theme: 'nature' }); // 触发类型错误,符合预期
myThingsRepo.addThing('Smartphone', 'Google Pixel 7', { color: 'blue', price: 10 }); // 无类型错误,符合预期

原理说明

  • 重构后的PropsMapper通过Extract工具类型,针对每个type键精准提取对应的Thing子类型,确保TypeScript能明确每个类型下的id集合。
  • 函数签名中使用THING['type']作为TYPE的约束,替代原有的keyof PropsMapper<THING>,让TypeScript能直接关联类型与对应的id、props结构,消除索引错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 21:25:30