基于id和label属性扁平化对象的TypeScript严格类型实现问题
TypeScript对象扁平化的类型约束实现
需求
需要将如下结构的对象:
{ article: 'prova', id: 63, topology: { id: 'topId', label: 'topLabel' }, something: { id: 'someId', label: 'someLabel' } }
扁平化为:
{ article: "prova", id: 63, topId: "topLabel", someId: "someLabel" }
类型约束要求
输入和输出必须满足严格的TypeScript类型约束,示例代码如下:
interface Input { article: string id: number abc: { id: string; label: string } def: { id: string; label: string } } interface Output { article: string id: number topId: string someId: string } const input1: Input = { article: 'prova', id: 63, abc: { id: 'topId', label: 'topLabel' }, def: { id: 'someId', label: 'someLabel' } } const input2 = { article: 'prova', id: 63, abc: { id: 'topId', label: 'topLabel' }, def: { id: 'someId', label: 'someLabel' } } // 报错信息: // Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, Record<string, string | number | { id: string; label: string; } | null | undefined>>' is missing the following properties from type 'Output': article, id, topId, someId // Argument of type 'Input' is not assignable to parameter of type 'Record<string, string | number | { id: string; label: string; } | null | undefined>'. // Index signature for type 'string' is missing in type 'Input'. const output1: Output = flattenObject(input1) // 报错信息: // Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, { article: string; id: number; abc: { id: string; label: string; }; def: { id: string; label: string; }; }>' is missing the following properties from type 'Output': topId, someId const output2: Output = flattenObject(input2)
我的尝试实现(存在类型错误)
我写了以下代码,但TypeScript提示上述类型错误:
function hasId< K extends string | number | undefined | null, T extends { id: string; label: string } >(el: [string, K | T]): el is [string, T] { return (el[1] as T).id != null } type Flattened< K extends string | number | undefined | null, T extends { id: string; label: string }, S extends Record<string, K | T> > = { [key in keyof S as S[key] extends T ? S[key]['id'] : key]: S[key] extends T ? S[key]['label'] : S[key] } const flattenObject = < K extends string | number | undefined | null, T extends { id: string; label: string }, S extends Record<string, K | T> >( obj: S ): Flattened<K, T, S> => Object.fromEntries( Object.entries(obj).map<[string, K | string]>((el) => hasId(el) ? [el[1].id, el[1].label] : (el as [string, K]) ) ) as Flattened<K, T, S>
解决方案
问题出在几个核心点:
S extends Record<string, ...>要求输入对象必须有字符串索引签名,但Input接口没有,导致类型不兼容。Flattened类型中直接引用S[key]['id']是值层面的操作,无法作为静态类型的键名,需要用条件类型提取类型信息。
以下是修正后的实现:
// 类型守卫:判断值是否为带id和label的对象 function isLabeledObject(value: unknown): value is { id: string; label: string } { return typeof value === 'object' && value !== null && 'id' in value && 'label' in value; } // 定义扁平化后的类型: // - 原对象中符合{id: string; label: string}的属性,替换为[id的静态类型: label的类型] // - 其他属性保持原键值对 type FlattenedObject<Obj> = { [K in keyof Obj as Obj[K] extends { id: infer Id extends string; label: infer _ } ? Id : K]: Obj[K] extends { id: infer _; label: infer Label } ? Label : Obj[K] }; function flattenObject<Obj extends Record<string, unknown>>(obj: Obj): FlattenedObject<Obj> { return Object.fromEntries( Object.entries(obj).map(([key, value]) => { if (isLabeledObject(value)) { return [value.id, value.label]; } return [key, value]; }) ) as FlattenedObject<Obj>; } // 测试验证 interface Input { article: string id: number abc: { id: 'topId'; label: string } def: { id: 'someId'; label: string } } interface Output { article: string id: number topId: string someId: string } const input1: Input = { article: 'prova', id: 63, abc: { id: 'topId', label: 'topLabel' }, def: { id: 'someId', label: 'someLabel' } }; const output1: Output = flattenObject(input1); // 类型匹配,无报错 const output2 = flattenObject({ article: 'prova', id: 63, abc: { id: 'topId', label: 'topLabel' }, def: { id: 'someId', label: 'someLabel' } }); // output2类型自动推导为:{ article: string; id: number; topId: string; someId: string }
关键修正说明
- 用
Obj extends Record<string, unknown>替代原有约束,兼容所有对象类型(包括无索引签名的接口)。 - 通过
infer关键字提取id的静态类型作为新键名,确保类型推导的准确性。 - 简化并严谨化类型守卫函数,避免嵌套泛型带来的类型复杂度。
内容的提问来源于stack exchange,提问作者darkbasic
相关产品推荐
相关产品推荐

