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
相关产品推荐
相关产品推荐

