Skip to content

Instantly share code, notes, and snippets.

@rymizuki
Created August 30, 2025 09:28
Show Gist options
  • Select an option

  • Save rymizuki/8ae555138f3be37f0d83c9e25b4b6244 to your computer and use it in GitHub Desktop.

Select an option

Save rymizuki/8ae555138f3be37f0d83c9e25b4b6244 to your computer and use it in GitHub Desktop.
import { isArray, isObject, isString } from '@/type-guards';
import type { CamelizeRecursive } from '@/type-utils/camelize';
import { camelize } from '../string/camelize';
/**
* 指定したオブジェクトのプロパティを再起的にcamelizeする
*
* @example
* ```
* expect(camelizeRecursive({ prop_a: 1, prop_b: 2 }))
* .toStrictEquals({ propA: 1, propB: 2 })
* ```
*/
export function camelizeRecursive<T = unknown>(input: T): CamelizeRecursive<T> {
if (input === null || input === undefined)
return input as CamelizeRecursive<T>;
if (!isObject(input)) {
return input as CamelizeRecursive<T>;
}
if (isArray(input)) {
return input.map((item) =>
isObject(item) ? camelizeRecursive(item) : item,
) as CamelizeRecursive<T>;
}
const result = {} as Record<string, unknown>;
const keys = Object.keys(input) as Array<keyof T>;
for (const prop of keys) {
const value = input[prop];
const key = isString(prop) ? camelize(prop) : prop;
result[key as string] = camelizeRecursive(value);
}
return result as CamelizeRecursive<T>;
}
import type { Camelize } from '@/type-utils/camelize';
/**
* 与えられた文字列をcamelCaseに変換する
*
* @example
* ```
* expect(camelize('some_value')).toBe('someValue')
* ```
*
*/
export function camelize<T extends string>(input: T): Camelize<T> {
let value = input as string;
if (value.startsWith('_')) return value as Camelize<T>;
if (/[A-Z]/.test(value)) {
value = value
.replace(/^([A-Z]+)/, (_, x: string) => x.toLowerCase())
.replaceAll(/([A-Z]+)/g, (_, x: string) => `_${x.toLowerCase()}`);
}
return value
.toLowerCase()
.replaceAll(/[.-\s_]+(\w|$)/g, (_, x: string) =>
x.toUpperCase(),
) as Camelize<T>;
}
import { describe, expect, test } from 'vitest';
import { camelizeRecursive } from './camelize-recursive';
describe('utils / object / camelizeRecursive', () => {
test.each([
[{ a: { a_b: { ab_c: null } } }, { a: { aB: { abC: null } } }],
[{ a: { a_b: [{ ab_c: null }] } }, { a: { aB: [{ abC: null }] } }],
[null, null],
[undefined, undefined],
[1000, 1000],
['snake_case', 'snake_case'],
['camelCase', 'camelCase'],
])('camelizeRecursive(%j) => %j', (input, expected) => {
expect(camelizeRecursive(input)).toStrictEqual(expected);
});
});
import { describe, expect, test } from 'vitest';
import { camelize } from './camelize';
describe('utils / string / camelize', () => {
test.each([
['example', 'example'],
['snake_case', 'snakeCase'],
['PascalCase', 'pascalCase'],
['kebabu-case', 'kebabuCase'],
['space between', 'spaceBetween'],
['UPPER', 'upper'],
['UPPER_CASE', 'upperCase'],
['_ignored_value', '_ignored_value'],
])('camelize(%s) => %s', (input, expected) => {
expect(camelize(input)).toStrictEqual(expected);
});
});
import { describe, expect, test } from 'vitest';
import { decamelizeRecursive } from './decamelize-recursive';
describe('decamelizeRecursive', () => {
describe('separator is default', () => {
test.each([
[{ a: { aB: { abC: null } } }, { a: { a_b: { ab_c: null } } }],
[{ a: { aB: [{ abC: null }] } }, { a: { a_b: [{ ab_c: null }] } }],
[null, null],
[undefined, undefined],
[1000, 1000],
['snake_case', 'snake_case'],
['camelCase', 'camelCase'],
])('decamelizeRecursive(%j) => %j', (input, expected) => {
expect(decamelizeRecursive(input)).toStrictEqual(expected);
});
});
describe("separator is '-'", () => {
test.each([
[{ a: { aB: { abC: null } } }, { a: { 'a-b': { 'ab-c': null } } }],
[{ a: { aB: [{ abC: null }] } }, { a: { 'a-b': [{ 'ab-c': null }] } }],
[null, null],
[undefined, undefined],
[1000, 1000],
['snake_case', 'snake_case'],
['camelCase', 'camelCase'],
])('decamelizeRecursive(%j) => %j', (input, expected) => {
expect(decamelizeRecursive(input, '-')).toStrictEqual(expected);
});
});
});
import { isArray, isObject, isString } from '@/type-guards';
import type { DecamelizeRecursive } from '@/type-utils/decamelize';
import { decamelize } from '../string/decamelize';
/**
* 指定したオブジェクトのプロパティを再起的にdecamelizeする
*
* @example
* ```
* expect(decamelizeRecursive({ propA: 1, propB: 2 }))
* .toStrictEquals({ prop_a: 1, prop_b: 2 })
* ```
*/
export function decamelizeRecursive<T = unknown, S extends string = '_'>(
input: T,
separator: S = '_' as S,
): DecamelizeRecursive<T, S> {
if (input === null || input === undefined)
return input as DecamelizeRecursive<T, S>;
if (!isObject(input)) return input as DecamelizeRecursive<T, S>;
if (isArray(input)) {
return input.map((item) =>
isObject(item) ? decamelizeRecursive(item, separator) : item,
) as DecamelizeRecursive<T, S>;
}
const result = {} as Record<string, unknown>;
const keys = Object.keys(input) as Array<keyof T>;
for (const prop of keys) {
const value = input[prop];
const key = isString(prop) ? decamelize(prop, separator) : prop;
result[key as string] = decamelizeRecursive(value, separator);
}
return result as DecamelizeRecursive<T, S>;
}
import { describe, expect, test } from 'vitest';
import { decamelize } from './decamelize';
describe('utils / string / decamelize', () => {
describe('separator is default', () => {
test.each([
['example', 'example'],
['snake_case', 'snake_case'],
['camelCase', 'camel_case'],
['PascalCase', 'pascal_case'],
['kebabu-case', 'kebabu_case'],
['space between', 'space_between'],
['UPPER', 'upper'],
['UPPER_CASE', 'upper_case'],
['_ignored_value', '_ignored_value'],
])('decamelize(%s) => %s', (input, expected) => {
expect(decamelize(input)).toStrictEqual(expected);
});
});
});
import type { Decamelize } from '@/type-utils/decamelize';
import { camelize } from './camelize';
/**
* 与えられた文字列をcamelCaseを分解し、任意の文字列(初期値は`_`)で結合する。
*
* @example
* ```
* expect(decamelize('someValue')).toBe('some_value')
* ```
*/
export function decamelize<T extends string, S extends string = '_'>(
input: T,
separator: S = '_' as S,
): Decamelize<T, S> {
let value = input as string;
if (value.startsWith('_')) return value as Decamelize<T, S>;
value = camelize(input);
return value.replaceAll(
/[A-Z]/g,
(x) => `${separator}${x.toLowerCase()}`,
) as Decamelize<T, S>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment