es-toolkit 的 toCamelCaseKeys 详解:递归将对象与数组键转换为 camelCase
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
toCamelCaseKeys是 es-toolkit 提供的对象工具函数,它将对象、嵌套对象以及数组中的对象的所有键递归转换为 camelCase(小驼峰)命名,并返回一个全新的对象。它尤其适合处理来自后端 API 的snake_case、PascalCase或全大写命名的数据,让你在接入前端代码时无需手工逐层改写字段名。读完本文,你将掌握它的转换规则、递归边界、类型推导能力以及源码级实现原理。
camelCase 命名规则
camelCase(小驼峰)是一种命名约定:标识符的第一个单词全部小写,后续每个单词的首字母大写并直接拼接,中间不使用任何分隔符。例如user_id转为userId,FIRST_NAME转为firstName。
const camelCased = toCamelCaseKeys(obj);使用方式
toCamelCaseKeys(obj)
当你需要把对象的所有键转换为 camelCase 时,直接调用toCamelCaseKeys即可。嵌套对象、数组内的对象都会被递归转换。
键的转换遵循以下规则:
snake_case→camelCase(例如user_id→userId)PascalCase→camelCase(例如UserId→userId)- 全大写键 →
camelCase(例如FIRST_NAME→firstName,LAST→last)
import { toCamelCaseKeys } from 'es-toolkit/object'; // 基本对象转换 const obj = { user_id: 1, first_name: 'John', last_name: 'Doe' }; const result = toCamelCaseKeys(obj); // result 为 { userId: 1, firstName: 'John', lastName: 'Doe' } // 数组内的对象也会被转换 const users = [ { user_id: 1, first_name: 'John' }, { user_id: 2, first_name: 'Jane' }, ]; const convertedUsers = toCamelCaseKeys(users); // convertedUsers 为 [{ userId: 1, firstName: 'John' }, { userId: 2, firstName: 'Jane' }] // 嵌套对象被完整转换 const nested = { user_data: { user_id: 1, contact_info: { email_address: 'john@example.com', phone_number: '123-456-7890', }, }, }; const nestedResult = toCamelCaseKeys(nested); // nestedResult 为 { // userData: { // userId: 1, // contactInfo: { // emailAddress: 'john@example.com', // phoneNumber: '123-456-7890' // } // } // } // PascalCase 与全大写键也会被转换 const raw = { UserId: 1, FIRST_NAME: 'JinHo', LAST: 'Yeom' }; const converted = toCamelCaseKeys(raw); // converted 为 { userId: 1, firstName: 'JinHo', last: 'Yeom' }参数
obj(T):需要将键转换为 camelCase 的对象、数组或原始值。
返回值
(ToCamelCaseKeys<T>):返回所有键均转换为 camelCase 的新对象。注意返回值是新对象,原对象不会被修改。
源码实现:递归与分支处理
从源码结构看,toCamelCaseKeys的实现位于 src/object/toCamelCaseKeys.ts,整体逻辑是对三种输入分别处理:
export function toCamelCaseKeys<T>(obj: T): ToCamelCaseKeys<T> { if (isArray(obj)) { return obj.map(item => toCamelCaseKeys(item)) as ToCamelCaseKeys<T>; } if (isPlainObject(obj)) { const result = {} as ToCamelCaseKeys<T>; const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const camelKey = camelCase(key) as keyof typeof result; const convertedValue = toCamelCaseKeys(obj[key]); result[camelKey] = convertedValue as ToCamelCaseKeys<T>[keyof ToCamelCaseKeys<T>]; } return result; } return obj as ToCamelCaseKeys<T>; }实现要点如下:
- 数组分支:通过
isArray判断后,用map对每个元素递归调用toCamelCaseKeys,因此数组中的对象及其嵌套结构都会被转换; - 普通对象分支:通过
isPlainObject判断(导入自 src/predicate/isPlainObject.ts),用Object.keys遍历自有可枚举键,将每个键交给camelCase转换后写入新对象,同时对该键的值递归转换; - 原始值兜底:非数组、非普通对象的值(数字、字符串、布尔值、
null、undefined等)原样返回,不做任何处理。
键转换的底层:camelCase 与单词切分
键名转换最终由 src/string/camelCase.ts 完成。它先用words函数按 src/string/words.ts 中的CASE_SPLIT_PATTERN正则将字符串切分为单词数组,再取第一个单词小写、后续单词首字母大写拼接:
export function camelCase(str: string): string { const words = getWords(str); if (words.length === 0) { return ''; } const [first, ...rest] = words; return `${first.toLowerCase()}${rest.map(word => capitalize(word)).join('')}`; }CASE_SPLIT_PATTERN基于 Unicode 属性(\p{Lu}、\p{Ll}、\p{Emoji_Presentation}等)匹配单词,这意味着支持 unicode 字符与 emoji。例如camelCase('HTTPRequest')会得到httpRequest,camelCase('Keep unicode 😅')会得到keepUnicode😅。这也是FIRST_NAME这种全大写键能正确切成FIRST、NAME两个词的原因。
类型层面:ToCamelCaseKeys 的递归类型推导
toCamelCaseKeys的返回值类型是ToCamelCaseKeys<T>,定义于 src/types/ToCamelCaseKeys.ts。它是一组递归的条件类型,与运行时行为严格对齐:
type SnakeToCamel<S extends string> = S extends `${infer H}_${infer T}` ? `${Lowercase<H>}${Capitalize<SnakeToCamel<T>>}` : Lowercase<S>; type PascalToCamel<S extends string> = S extends `${infer F}${infer R}` ? `${Lowercase<F>}${R}` : S; type AnyToCamel<S extends string> = S extends `${string}_${string}` ? SnakeToCamel<S> : S extends Uppercase<S> ? Lowercase<S> : PascalToCamel<S>; export type ToCamelCaseKeys<T> = T extends NonPlainObject ? T : T extends any[] ? Array<ToCamelCaseKeys<T[number]>> : T extends Record<string, any> ? { [K in keyof T as AnyToCamel<Extract<K, string>>]: ToCamelCaseKeys<T[K]> } : T;类型层的三条规则与运行时分支一一对应:
AnyToCamel用模板字面量类型处理三种键:含下划线的按SnakeToCamel递归转换;全大写的整体Lowercase;其余(含 PascalCase)仅将首字母小写;- 映射类型
{ [K in keyof T as AnyToCamel<...>]: ... }通过as重映射键名,同时递归转换值类型,因此嵌套对象和数组在编译期就能得到精确的类型; NonPlainObject(定义于 src/_internal/NonPlainObject.ts)列出了Date、RegExp、Map、Set、Promise、Error、ArrayBuffer、各类 TypedArray、函数以及globalThis等内置对象类型,这些类型直接原样通过,不做键转换,避免对内置对象的内部结构做无意义且危险的映射。
type Response = { user_id: number; first_name: string }; type Converted = ToCamelCaseKeys<Response>; // => { userId: number; firstName: string }该类型在 docs/types/reference/objects/ToCamelCaseKeys.md 中有独立文档说明。
边界行为与测试验证
src/object/toCamelCaseKeys.spec.ts 用 Vitest 覆盖了完整的行为边界,可作为实际使用时的参考:
- 数组内对象与对象内数组双向递归:
{ user_list: [...] }中数组元素的键同样被转换; - 原始值原样返回:
123、'string'、null、undefined、true传入后均原样返回; - 空对象与空数组:
{}和[]分别返回{}和[]; - 保留原型方法:值为
Object.prototype.toString这类函数属性的键不会被破坏; - 不递归非普通对象:
Date、Map、Set、RegExp等值只作为普通值保留,created_at: date只把键转为createdAt,值仍是同一个Date实例; - 类型级验证:测试通过
expectTypeOf断言嵌套对象、数组、混合结构与全大写键(如as const的FIRST_NAME)在编译期都得到正确的 camelCase 类型。
与 lodash/fp 的对比基准
仓库在 benchmarks/performance/toCamelCaseKeys.bench.ts 中提供了针对深层嵌套对象的性能对比基准,与lodash/fp的mapKeys(camelCase)组合在同一结构上对比(lodash 方案为浅层转换)。基准数据使用 Vitest 的bench运行,你可以在仓库中查看该文件了解对比设置与数据结构。需要注意的是,lodash/fp 方案默认只转换顶层键,而 es-toolkit 的toCamelCaseKeys是深度递归转换,二者行为并不完全等价。
使用建议
toCamelCaseKeys从 src/object/index.ts 导出,可通过import { toCamelCaseKeys } from 'es-toolkit/object'按需引入,也可从es-toolkit主入口导入;- 它返回新对象,适合用于 API 响应数据的清洗层,例如在请求封装里统一对响应体做
toCamelCaseKeys转换,再交给业务代码使用; - 由于转换是深度递归的,对于超大对象会有一定的遍历开销,可结合实际数据规模评估是否需要在边缘层缓存结果;
- 若需要反向转换(camelCase → snake_case),仓库还提供了对应的
toSnakeCaseKeys,与本文的转换规则互为镜像,可配合使用。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考