如何不使用as const从深度嵌套对象推断字面量类型?
问题背景
你想要实现一个LeafValues工具类型,从嵌套对象中提取最深层的非对象值,并且保留它们的字面量类型(比如"hello"而非string)。当前的实现如下:
type LeafValues<T extends object> = { [K in keyof T]: T[K] extends object ? LeafValues<T[K]> : T[K]; }[keyof T];
但直接使用时,TypeScript会把字面量类型拓宽为原始类型:
const obj = { foo: "hello", bar: { baz: "world", }, }; type Result = LeafValues<typeof obj>; // 实际为 string,预期是 "hello" | "world"
虽然用as const可以解决,但你不想给每个对象都加这个断言。
核心限制:TypeScript的类型拓宽机制
问题的根源在于TypeScript的字面量拓宽规则:当你直接声明一个对象变量时,TypeScript会自动把字面量类型(比如"hello")拓宽为对应的原始类型(string),目的是让变量后续可以被重新赋值。只有当你用as const断言,或者通过特定方式(比如const泛型)捕获类型时,才会保留字面量的精确类型。
LeafValues工具类型只能基于传入的类型参数工作,而typeof obj已经是拓宽后的{ foo: string; bar: { baz: string } },所以类型系统无法从这个拓宽后的类型还原出原始的字面量信息。
可行的变通方案
1. 使用const泛型辅助函数(推荐)
TypeScript 5.0及以上支持const泛型,可以让函数自动将传入的对象字面量按as const的方式推断类型,无需手动添加断言:
// 定义一个辅助函数,用const泛型捕获字面量类型 const createStrictObj = <const T extends object>(obj: T) => obj; // 创建对象时用这个函数包裹 const obj = createStrictObj({ foo: "hello", bar: { baz: "world", }, }); type Result = LeafValues<typeof obj>; // "hello" | "world" ✅
如果需要对象属性是可写的(const泛型会让属性变成只读),可以再加一层类型转换去掉readonly:
type Writable<T> = { -readonly [K in keyof T]: T[K] }; const createStrictObj = <const T extends object>(obj: T): Writable<T> => obj as Writable<T>; const obj = createStrictObj({ foo: "hello", bar: { baz: "world", }, }); obj.foo = "hi"; // 允许赋值,类型依然保留字面量约束 type Result = LeafValues<typeof obj>; // "hello" | "world" ✅
2. 手动指定对象的精确类型
如果不想用辅助函数,可以直接给对象指定包含字面量的类型定义,虽然繁琐但有效:
// 手动定义包含字面量的类型 type ExactObjType = { foo: "hello"; bar: { baz: "world"; }; }; // 让对象符合这个类型 const obj: ExactObjType = { foo: "hello", bar: { baz: "world", }, }; type Result = LeafValues<ExactObjType>; // "hello" | "world" ✅
结论
目前无法仅通过修改LeafValues类型本身实现需求,因为类型系统无法从拓宽后的原始类型逆向还原字面量信息。最便捷的方案是使用const泛型辅助函数,在对象创建阶段就保留字面量类型,同时避免手动添加as const。
内容的提问来源于stack exchange,提问作者Abdurezak Farah

