🐍

TypeScriptでスネークケースとケバブケースをキャメルケースに変換する

2023/12/23に公開

お疲れ様です。ポート株式会社サービス開発部でフロントエンドエンジニアをしているフジタです。

はじめに

私の所属するチームでは、フロントエンドにReactとTypeScript、バックエンドにRailsを採用しています。フロントエンドとバックエンド間のデータ通信にはAPIを使用していて、そのためRailsで標準的に用いられるスネークケースのオブジェクトキーを、APIでJSONデータを受け取る際にキャメルケースへと変換する必要があります。

https://eslint.org/docs/latest/rules/camelcase

この記事ではTypeScriptでスネークケースとケバブケースをキャメルケースに変換するコードの実装を行なったのでそれについてまとめていこうと思います。

完成したもの

結果的に完成したコードが以下になります。なるべく次にコードを触る人にわかりやすくするためJSDocも追加しました。

https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html

to_camel_case.ts
/**
 * キャメルケースに変換するための型定義
 * @template T オブジェクトの型
 * @typedef {object} CamelCase
 * - もしTがオブジェクト(Record<string, unknown>)ならば、その全てのキーをキャメルケースに変換する
 * - もしTが配列ならば、その要素を再帰的にキャメルケースに変換する
 * - それ以外の場合、Tをそのまま返す
 */
type CamelCase<T> = T extends Record<string, unknown>
  ? { [K in keyof T]: CamelCase<T[K]> }
  : T extends (infer U)[]
  ? U extends Record<string, unknown>
    ? CamelCase<U>[]
    : T
  : T;

/**
 * スネークケースやケバブケースの文字列をキャメルケースに変換する関数
 * @function toCamelCase
 * @template T オブジェクトの型
 * @param {T} obj スネークケースやケバブケースのキーを持つオブジェクト
 * @returns {CamelCase<T>} キャメルケースのキーを持つオブジェクト
 * @description
 * - もし入力が配列ならば、その要素を再帰的にキャメルケースに変換する
 * - もし入力がオブジェクトならば、そのキーをキャメルケースに変換する
 * - それ以外の場合、入力をそのまま返す
 */
const toCamelCase = <T extends Record<string, unknown>>(obj: T): CamelCase<T> => {
  if (Array.isArray(obj)) {
    return obj.map(toCamelCase) as unknown as CamelCase<T>;
  } if (obj !== null && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [
        key.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')),
        toCamelCase(value as Record<string, unknown>),
      ]),
    ) as unknown as CamelCase<T>;
  }
  return obj as CamelCase<T>;
};

export default toCamelCase;

コードの解説

以下に詳しいコードの処理を説明していこうと思います。

型定義 CamelCase<T>

to_camel_case.ts
type CamelCase<T> = T extends Record<string, unknown>
  ? { [K in keyof T]: CamelCase<T[K]> }
  : T extends (infer U)[]
  ? U extends Record<string, unknown>
    ? CamelCase<U>[]
    : T
  : T;

型定義ではCamelCase<T>という型エイリアスを定義しています。この型はジェネリック型Tを受け取り、条件付き型を用いて変換処理の対象となるデータの構造に応じて異なる型を返します。

データの構造 行う処理
オブジェクトの場合 オブジェクトの全てのキーをキャメルケースに変換
配列の場合 配列の要素を再帰的にキャメルケースに変換
それ以外 入力されたTをそのまま返す

詳しい処理の流れは以下のようになっています。

  1. Tがオブジェクト型かどうかチェックする
  2. オブジェクトなら各キーをCamelCaseにマッピングする
  3. Tがオブジェクトでない場合はTが配列かどうかチェックする
  4. U(配列の要素)がオブジェクトなら、配列に対して再帰的にCamelCaseを適用する
  5. Uがオブジェクトでない場合、元の配列型Tを返す
  6. Tがオブジェクトでも配列でもない場合、Tをそのまま返す

関数 toCamelCase

to_camel_case.ts
const toCamelCase = <T extends Record<string, unknown>>(obj: T): CamelCase<T> => {
  if (Array.isArray(obj)) {
    return obj.map(toCamelCase) as unknown as CamelCase<T>;
  } if (obj !== null && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [
        key.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')),
        toCamelCase(value as Record<string, unknown>),
      ]),
    ) as unknown as CamelCase<T>;
  }
  return obj as CamelCase<T>;
};

toCamelCase関数はジェネリック関数で、任意のオブジェクト型Tを引数として受け取り、そのオブジェクトのキーをキャメルケースに変換した新しいオブジェクトを返します。

詳しい処理の流れは以下のようになっています。

  1. 配列かどうかチェックして、配列なら各要素にtoCamelCaseを再帰的に適用する
  2. オブジェクトかどうかをチェックする
  3. オブジェクトのキーと値を処理し、新しいキャメルケースのオブジェクトを作成する
  4. キーをスネークケースやケバブケースからキャメルケースに変換する
  5. 値がオブジェクトの場合、再帰的にtoCamelCaseを適用する
  6. 配列でもオブジェクトでもない場合は、そのまま返す

JSDocを追加する

以下はJSDocのコードとJSDocのタグの説明です。

to_camel_case.ts
/**
 * キャメルケースに変換するための型定義
 * @template T オブジェクトの型
 * @typedef {object} CamelCase
 * - もしTがオブジェクト(Record<string, unknown>)ならば、その全てのキーをキャメルケースに変換する
 * - もしTが配列ならば、その要素を再帰的にキャメルケースに変換する
 * - それ以外の場合、Tをそのまま返す
 */
タグ 説明
@template ジェネリック型パラメータについて説明する
@typedef 新しい名前の型を定義する
to_camel_case.ts
/**
 * スネークケースやケバブケースの文字列をキャメルケースに変換する関数
 * @function toCamelCase
 * @template T オブジェクトの型
 * @param {T} obj スネークケースやケバブケースのキーを持つオブジェクト
 * @returns {CamelCase<T>} キャメルケースのキーを持つオブジェクト
 * @description
 * - もし入力が配列ならば、その要素を再帰的にキャメルケースに変換する
 * - もし入力がオブジェクトならば、そのキーをキャメルケースに変換する
 * - それ以外の場合、入力をそのまま返す
 */
タグ 説明
@function 関数名
@template ジェネリック型パラメータについて説明する
@param 関数が受け取る引数について説明する
@returns 関数の戻り値の型と簡単な説明をする
@description 関数、メソッド、クラス、またはその他のコードの部分についての詳細な説明をする

終わりに

これで、TypeScriptを使用してスネークケースやケバブケースをキャメルケースに変換するコードについての説明は以上になります。

同じような機能を提供するライブラリは既に存在しますが、学習の一環として自分で実装してみました。ジェネリック型やJSDocについての理解を少しでも深められたと思います。

https://www.npmjs.com/package/humps

最後までお読みいただき、ありがとうございました!

他にもポート株式会社のアドベントカレンダー記事が公開されていますので興味があればぜひ覗いてみてください!
https://adventar.org/calendars/9156

Discussion