改进深度嵌套属性类型的泛型推导机制
问题描述
我有一个能正确推导深度嵌套属性类型的泛型函数,但只有传入根对象时才生效。能不能改进这个函数,让它不用传入根对象(比如通过泛型参数定义对象类型)也能正常工作?
以下是可运行与不可运行的代码示例:
// WORKING PART type Flatten<T, O = never> = Writable<Cleanup<T>, O> extends infer U ? U extends O ? U : U extends object ? ValueOf<{ [K in keyof U]-?: (x: PrefixKeys<Flatten<U[K], O>, K, O>) => void }> | ((x: U) => void) extends (x: infer I) => void ? { [K in keyof I]: I[K] } : never : U : never; type Writable<T, O> = T extends O ? T : { [P in keyof T as IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>]: T[P] } type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B; type Cleanup<T> = 0 extends (1 & T) ? unknown : T extends readonly any[] ? (Exclude<keyof T, keyof any[]> extends never ? { [k: `${number}`]: T[number] } : Omit<T, keyof any[]>) : T; type PrefixKeys<V, K extends PropertyKey, O> = V extends O ? { [P in K]: V } : V extends object ? { [P in keyof V as `${Extract<K, string | number>}.${Extract<P, string | number>}`]: V[P] } : { [P in K]: V }; type ValueOf<T> = T[keyof T] function lookup<T, K extends keyof Flatten<T>>(obj: T, key: K): Flatten<T>[K]; function lookup(obj: any, key: string) { const i = key.indexOf("."); return (i < 0) ? obj[key] : (lookup as any)(obj[key.substring(0, i)], key.substring(i + 1)); } function lookupWithValue<ObjectType, DeepKeyType extends keyof Flatten<ObjectType>>(obj: ObjectType, key: DeepKeyType, value: Flatten<ObjectType>[DeepKeyType]): void { // SOME LOGIC } // SHOWCASE const foo = { a: { b: { c: 10 } }, d: [ 0, { e: { f: "f" } }, "20" ] }; type FooType = typeof foo; lookupWithValue(foo, "a.b.c", 10); // OK! Correct, because (number type == number type) lookupWithValue(foo, "a.b.c", "10"); // ERROR! Correct, because (string type != number type) // ===== // NOT WORKING PART // ===== // Is there a way to create a method, which doesn't require the root object to be passed into ??? function lookupWithValueWithoutObject<ObjectType, ...>(key: ???, value: ???): void { // SOME LOGIC } lookupWithValueWithoutObject<FooType>("a.b.c", 10); // OK lookupWithValueWithoutObject<FooType>("a.b.c", "10"); // ERROR
我尝试移除obj参数后,类型推导失效了——要么编译不通过,要么无法正确识别传入值和深度嵌套属性的类型不匹配错误。
解决方案
完全可以实现这个需求,只需要调整泛型参数的约束逻辑,让函数直接通过显式传入的对象类型泛型来推导嵌套属性的类型,无需依赖根对象参数。
修改后的函数代码如下:
function lookupWithValueWithoutObject< ObjectType, DeepKeyType extends keyof Flatten<ObjectType> >( key: DeepKeyType, value: Flatten<ObjectType>[DeepKeyType] ): void { // SOME LOGIC }
调用时只需显式指定对象类型泛型,TypeScript就能自动完成键的合法性校验和值的类型匹配:
lookupWithValueWithoutObject<FooType>("a.b.c", 10); // ✅ 类型匹配,正常通过 lookupWithValueWithoutObject<FooType>("a.b.c", "10"); // ❌ 类型不匹配,触发错误
核心原理
原函数是通过obj参数让TypeScript自动推断ObjectType,现在我们将ObjectType改为显式传入的泛型参数,后续的DeepKeyType依然基于Flatten<ObjectType>的键集合做约束,value的类型也依然关联Flatten<ObjectType>[DeepKeyType],因此类型校验逻辑完全保留,只是把类型推断的触发方式从参数推导改成了显式泛型传入。
需要注意的是,调用时必须显式指定ObjectType泛型(比如<FooType>),因为没有根对象参数可供TypeScript自动推断这个类型。
内容的提问来源于stack exchange,提问作者Mark Dolbyrev
相关产品推荐
相关产品推荐

