型安全性を高めるTypeScriptのリテラル型の活用例

2024/06/15に公開

はじめに

Webエンジニアとして働きはじめて約1ヶ月が経ちました。その中でフロントエンドを担当している際に、TypeScriptのリテラル型を使用したタイプセーフな実装方法を行なっているのを見て、とても便利だと思ったため活用例を用いて簡単にまとめてみようと思います。

型安全の重要性

「型安全」とは、プログラムが型に関するエラーを防ぐための仕組みのことを指します。JavaScriptは動的言語であり、コードが実行されてからエラーが発生するのに対して、静的言語のTypeScriptはコンパイル時にエラーを検出することができます。

そのため、以下のような恩恵を受けることができます。

  • エラーの早期発見
    型不一致のエラーをコンパイル時に検出することができます。
  • デバッグが容易
    型エラーがコンパイル時に明示的に表示されるため、エラーメッセージからバグの原因を特定することができます。
  • コードの一貫性
    チームでの開発時に型情報に基づいて開発を行うことで、コード全体の一貫性を維持しながら開発を進めることができます。

このように、型を定義することで高品質なコードを維持して、開発効率を向上させることができるため、メンテナンスのしやすいプロダクトを開発することができます。

リテラル型とは

「サバイバルTypeScript」 では以下のように説明されています。

TypeScriptではプリミティブ型の特定の値だけを代入可能にする型を表現できます。そのような型をリテラル型と呼びます。

https://typescriptbook.jp/reference/values-types-variables/literal-types

リテラル型を使うことで、オブジェクトや変数のプロパティに特定の値のみを使用できるように制約をかけることができます。
このようにすることで、予期しない値が設定されることを防ぐことができ、型安全性を高めることができます。

文字列リテラル

// 基本的な例
type Fruit = 'apple' | 'orange' | 'grape' | 'cherry';
let fruit: Fruit;
// 有効な値
fruit = 'apple';
// 無効な値
fruit = 'banana'; // error: Type '"banana"' is not assignable to type 'Fruit'.

また、文字列だけでなく「数値・真偽値」でもリテラル型を定義することができます。

数値リテラル

type Numbers = 1 | 2 | 3 | 4;
let number: Numbers;
// 有効な値
number = 1;
// 無効な値
number = 5; // error: Type '5' is not assignable to type 'Numbers'.

真偽値リテラル

type BooleanLiteral = true | false;
let booleanValue: BooleanLiteral;
// 有効な値
booleanValue = true;
booleanValue = false;
// 無効な値
booleanValue = "yes";  // error: Type 'string' is not assignable to type 'BooleanLiteral'.

リテラル型のメリット

リテラル型を使用することで、以下のような「開発効率の向上」や「コードの拡張性」などのメリットを得ることができます。

値の制約

先ほどの説明が重複してしまいますが、変数や関数の引数が特定の値だけを使用するように制限をかけることができます。
これにより無効な値の使用を防ぐことができ、予期せぬバグを未然に防ぐことができます。

型推論と自動補完

リテラル型を使用することにより、エディタが型情報をもとに自動で補完を行なってくれます。

安全な拡張

リテラル型を使用することで、新しい値の拡張を安全に行うことができます。リテラル型に値を定義しておき共通化しておけば、新しい値を設定する際にリテラル型のみを変更すればよくなるため、変更に強い設計にすることができます。

活用例

記事のカテゴリーをレスポンスで受け取り、カテゴリーごとにテーマカラーを設定する

[使用技術]

  • Next.js "14.2.4"
  • React "18"
  • TypeScript "5"
  • Tailwind CSS "3.4.1"

レスポンスで受け取りカテゴリー(string型)をリテラル型に定義することで、テーマカラーの定義・追加が容易になります。
また、フロント側で設定されていないカテゴリーに関しては、デフォルトのカラーを設定することができ、予期せぬカテゴリーのレスポンスが返ってきた場合のハンドリングも容易に行うことができます。

まず記事のカテゴリーをリテラル型で定義します。

types/category.ts
// 記事のカテゴリーをリテラル型で定義
export type Category = "technology" | "health" | "finance" | "lifestyle";

export type Article = {
  id: number;
  title: string;
  content: string;
  category: Category;
};

次に、カテゴリーごとのテーマカラーを設定する関数を作成します。

utils/category/color.ts
import { Category } from "@/types/category";

// テーマカラーを設定する関数
export function getBgThemeColor(category: Category): string {
  switch (category) {
    case "technology":
      return "bg-green-400"; // 緑
    case "health":
      return "bg-amber-500"; // オレンジ
    case "finance":
      return "bg-cyan-400"; // 青
    case "lifestyle":
      return "bg-yellow-500"; // 黄
    default:
      return "bg-rose-500"; // デフォルトカラー
  }
}

記事のダミーデータも作成しておきます。

testData/articles/index.ts
import { Article } from "@/types/category";

export const articlesData: Article[] = [
  { id: 0, title: "テストタイトル1", content: "...", category: "technology" },
  { id: 1, title: "テストタイトル2", content: "...", category: "health" },
  { id: 2, title: "テストタイトル3", content: "...", category: "finance" },
  { id: 3, title: "テストタイトル4", content: "...", category: "lifestyle" },
];

では、実際にコンポーネントで記事データを取得して、カテゴリー名の背景色にテーマカラーを設定してみます。

親コンポーネント

app/literal/page.tsx
import ArticleCard from "@/app/literal/_components/article-card";
import { articlesData } from "@/testData/articles";

export default function Page() {
  // ダミーデータを取得
  const articles = articlesData;
  return (
    <div className="px-4 grid gap-y-4">
      {articles.map((article) => (
        <ArticleCard
          key={article.id}
          title={article.title}
          category={article.category}
        />
      ))}
    </div>
  );
}

子コンポーネント

app/literal/_components/article-card.tsx
import { Article } from "@/types/category";
import { getBgThemeColor } from "@/utils/category/color";

export default function ArticleCard({
  title,
  category,
}: Pick<Article, "title" | "category">) {
  // テーマカラーを設定する関数にpropsで渡ってきたcategoryを引数に渡します。
  const categoryBgColor = getBgThemeColor(category);
  return (
    <div>
      <h2 className="font-bold text-xl">{title}</h2>
      // getBgThemeColor関数から返ってきたclassNameを設定します。
      <p className={`${categoryBgColor} w-fit p-1`}>{category}</p>
    </div>
  );
}

以下のようにカテゴリー名にテーマカラーを設定することができました。

Image from Gyazo

このようにレスポンスで受け取る値をリテラル型に定義すると、新規にカテゴリーが追加されてテーマカラーを追加する変更があった場合に、utils/category/color.tsgetBgThemeColor 関数のみを変更するだけで良くなるため、ヒューマンエラーを減らすことができます。

Enumを用いた方法

先ほどの活用例をEnumを用いた実装方法に変更することで、コードの可読性の向上と一貫性の確保を行うことができます。

まず、Enumを用いた型定義を行います。

types/category.ts
// Enumを用いた型定義
export const CategoryEnum = {
  TECHNOLOGY: "technology",
  HEALTH: "health",
  FINANCE: "finance",
  LIFESTYLE: "lifestyle",
} as const;

export type CategoryEnum = (typeof CategoryEnum)[keyof typeof CategoryEnum];

次に、Enumを用いてテーマカラーを設定する関数を定義します。

utils/category/color.ts
// Enumを用いたテーマカラーを設定する関数
export function getBgThemeColorEnum(category: string) {
  return bgThemeBgColor[category] || "bg-rose-500";
}

export const bgThemeBgColor: { [key: string]: string } = {
  [CategoryEnum.TECHNOLOGY]: "bg-green-400",
  [CategoryEnum.HEALTH]: "bg-amber-500",
  [CategoryEnum.FINANCE]: "bg-cyan-400",
  [CategoryEnum.LIFESTYLE]: "bg-yellow-500",
};

このようにEnumを用いた型定義を行うと、定数の用途が明確になり、switch文に比べてコードの可読性を向上させることができます。特に、複数の定数を作成する場合に、Enumを使用することでそれぞれのグループ化された定数であることが明確になり、今後の変更に強い設計にすることができます。

まとめ

今回は、TypeScriptのリテラル型を使用したタイプセーフな実装方法についてまとめました。TypeScriptへの理解はまだ浅いですが、型定義を行うことの重要性が徐々に分かってきた気がします。

リテラル型は、特定の値だけを使用できるように制限をかけることができるため、サービスの運用中に変更があった場合のヒューマンエラーを減らすことができるのではないかと思います。また、Enumを用いて定数を一つの名前空間にまとめることで、コードの可読性と一貫性を向上させることができる点も非常に便利です。

これからは、さらにTypeScriptの強力な型システムについて学び、より高品質なコードを書くためのスキルを身につけていきたいと思います。

最後に、Xで駆け出しエンジニア目線で学んだことを発信していますので、良かったらフォローしていただけると嬉しいです。

https://twitter.com/ippei_111

参考資料

https://typescriptbook.jp/reference/values-types-variables/literal-types

https://zenn.dev/oreo2990/articles/65be8a24e842be

GitHubで編集を提案

Discussion