TypeScript使用keyof索引二维对象报TS2536错误的解决方法
有存储多语言翻译映射的二维对象,结构示例:
const translations = { 'login': { 'e-mail': 'E-Mail', 'username': 'Username', 'forgot-password': 'Forgot your password?' }, 'dashboard': { 'hello {x}': 'Hello {x} how is your day?', 'go-to-other-page': 'Go to page...' } }
预先定义AllTranslations接口声明所有翻译分类、下属文案的类型结构:
interface AllTranslations { login: {} // 存储login分类下所有文案 dashboard: {} // 存储dashboard分类下所有文案 }
基于该接口实现translate翻译函数,根据传入的分类key、文案key、插值参数返回对应翻译文本:
type TranslationCategory<T = keyof AllTranslations> = keyof AllTranslations[T]; const translate = <T extends keyof AllTranslations>(key: T, message: TranslationCategory<T>, params: {[key: string]: string|number} = {} ): string => { const m = translations[key][message] ?? message; // 执行文案格式化、插值参数替换逻辑 return m as string; }
函数调用时分类、文案字段均可正常触发IDE自动补全:
translate('login', 'username'); translate('dashboard', 'hello {x}', {x: 'User'});
但TranslationCategory类型定义行抛出TS编译错误:TS2536: Type 'T' cannot be used to index type 'AllTranslations',需要在不使用//@ts-ignore忽略错误的前提下修复问题,保留完整的自动补全能力。
类型别名TranslationCategory的泛型参数T只设置了默认值为keyof AllTranslations,但没有给泛型本身添加类型约束。TypeScript无法确定传入的T一定是AllTranslations的合法索引类型,因此不允许执行AllTranslations[T]索引操作,抛出编译错误。
给TranslationCategory的泛型参数T添加extends keyof AllTranslations约束,明确T的类型范围一定是AllTranslations的合法key,即可消除编译错误,同时完全保留原有的类型校验、IDE自动补全能力。
修正后的完整代码如下:
interface AllTranslations { login: {} dashboard: {} } // 给泛型T添加类型约束,明确其一定是AllTranslations的合法key type TranslationCategory<T extends keyof AllTranslations = keyof AllTranslations> = keyof AllTranslations[T]; const translate = <T extends keyof AllTranslations>( key: T, message: TranslationCategory<T>, params: {[key: string]: string|number} = {} ): string => { const m = translations[key][message] ?? message; // 插值替换、格式化逻辑 return Object.entries(params).reduce( (result, [paramKey, paramValue]) => result.replaceAll(`{${paramKey}}`, String(paramValue)), m as string ); }
可选优化
如果不想手动维护AllTranslations接口和实际translations对象的字段同步,可以直接用typeof从已定义的翻译常量对象推导类型,避免两边字段不一致导致的类型错误:
// 直接从translations常量推导全量翻译类型,无需手写接口 type AllTranslations = typeof translations; type TranslationCategory<T extends keyof AllTranslations = keyof AllTranslations> = keyof AllTranslations[T]; const translate = <T extends keyof AllTranslations>( key: T, message: TranslationCategory<T>, params: {[key: string]: string|number} = {} ): string => { const m = translations[key][message] ?? message; return Object.entries(params).reduce( (result, [paramKey, paramValue]) => result.replaceAll(`{${paramKey}}`, String(paramValue)), m ); }
该方案下,后续给translations对象新增分类、新增文案时,类型会自动同步,自动补全、类型校验会同步更新,无需手动修改接口定义。
内容的提问来源于stack exchange,提问作者Robin Schambach

