0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】合併型・リテラル型を活用した業務ルールの表現

0
Posted at

はじめに

最近、TypeScriptを実務で活用するようになり、改めて体系的に学ぼうと思い、『初めてのTypeScript』という書籍で勉強しています。

その中で、合併型(Union Types)とリテラル型(Literal Types)の解説が特に勉強になりました。

これらの型は、単に変数に代入できる値を制限するだけでなく、アプリケーションで扱う状態やデータの制約を表現するうえでも活用できます。

そこで本記事では、合併型・リテラル型の基本を整理したうえで、実務での活用方法や、型によってドメイン上の制約をどこまで表現できるのかを考えていきます。

対象読者

  • TypeScriptの基本的な型注釈を理解している人
  • 合併型やリテラル型の使い方を学びたい人
  • 型を実務の設計にどう活かすか考えたい人
  • 型による制約と、実行時のバリデーションの違いを理解したい人

合併型・リテラル型って?

初学者向けに、まずは合併型・リテラル型についてそれぞれ説明しておこうと思います。

合併型(Union Types)

合併型は、複数の型のうち、いずれかに該当する値を表現する型のことです。

例えば、次のコードを見てみましょう。

let value: string | number;
value = "hello";
value = 123;

string | numberの|は「または」や「OR」のような意味合いです。
このコードの場合、変数valueに代入できる値はstring型かnumber型の値のみです。

合併型を考える時に重要なのは、その変数に代入できる値が指定された型のいずれかに該当しないといけないという点です。

注意点
合併型を扱う時はその関数の中の処理にも注意しましょう。
例えば、以下のコードのようにtoUpperCase()のように文字列メソッドを扱おうとするとエラーになります。

function printValue(value: string | number) {
  console.log(value.toUpperCase());
}

image.png

リテラル型(Literal Types)

リテラル型は、特定の文字列や数値など、具体的な値をそのまま型として扱う仕組みのことです。
例えば、次のコードを見てみましょう。

let articleStatus: "draft";

articleStatus = "draft";     // OK
articleStatus = "published"; // エラー

TS Playgroundで確認してみるとこんな感じです。
変数articleStatusにpublishedという値が入ることは許容されていないのでエラーが出ています。

image.png

通常のstring型だといろんな文字列を代入できますが、
このように記載しておくと、特定の値以外を代入されることを防ぐことができます。

合併型とリテラル型を組み合わせてみよう

ここで一度、それぞれの概念について振り返ってみましょう。
合併型は、さまざまな文字列を許容する複数の型のうち、いずれかの値があることを表現する型でした。
リテラル型は、特定の文字列や数値など、具体的な値そのものを型として扱う仕組みでした。

これらを組み合わせると、より実用的なことが可能になると思いませんか?

例えば、自身のブログサイトを作成することを考えてみましょう。
以下のコードのように記事のステータスを管理することができれば、より安全に記事の管理ができます。

type Status = "draft" | "published";

let status: Status;

status = "draft";     // OK
status = "published"; // OK
status = "deleted";   // エラー

この型は、"draft" または "published"がstatusに代入できるということを表現しています。
string型で許容するような仕組みにするのではなく、このようにリテラル型を組み合わせることで、許容する状態を明示できることが実務では非常に重要になってきます。

ドメイン制約を型で表現するなら?

ドメイン制約とは、特定の業務領域において、データや操作が満たすべきルールや条件のことです。実務では、単にデータを保存・取得するだけでなく、「どのような値を許容するのか」「どのような状態を正しいとするのか」といった、業務上のルールを考慮して設計する必要があります。

例えば、ブログサイトで記事を管理する場合を考えてみましょう。

記事にはタイトルやステータス、公開日時などの情報があります。このとき、業務上のルールとして、次のような条件が考えられます。

  • 記事のタイトルは文字列である
  • 記事のステータスは「下書き」または「公開済み」である
  • 公開済みの記事には、公開日時が設定されている
  • 下書きの記事は、公開日時が設定されていない

これらのルールを実装する際、まずは TypeScript の型を使って、記事が持つデータの構造を定義できます。

type Article = {
  id: string;
  title: string;
  articleStatus: "draft" | "published";
  publishedAt?: Date;
};

この型では、title を文字列に限定し、status には "draft" または "published" のいずれかを指定する必要があります。publishedAt は Date 型の値を設定できますが、必須ではありません。

このように型を定義することで、単に string や Date といったデータの型を指定するだけでなく、記事として許容する値の範囲をコード上で明示できます。

踏み込んで考えてみましょう

実は、先ほど挙げたArticle型は要件を完全に満たしていませんでした。
どこが不完全だったかわかるでしょうか?

先ほどの要件では、公開済みの記事では、公開日時が必要でした。ですが、現在の型定義では次のようなデータが許容されてしまいます。

const article: Article = {
  id: "article-001",
  title: "TypeScriptの型について",
  articleStatus: "published",
};

statusはpublished ですが、publishedAtが設定されていません。つまり、型チェックを通過しても、業務上のルールを満たさないデータが存在し得るということです。

では、記事のステータスに応じて必要なデータを変えるには、どのような型設計が必要でしょうか?

型設計を見直そう

では、先ほどの要件を満たすには、どのように型を設計すればよいでしょうか。
ひとつの方法は、記事のステータスごとに型の構造を分けることです。
型定義は合併型を使って2通りにしてみました。
これで、draftのケース、publishedのケースをそれぞれ表現することができました。

type ArticleBase = {
  id: string;
  title: string;
};

type Article =
  | (ArticleBase & {
      articleStatus: "draft";
      publishedAt?: never;
    })
  | (ArticleBase & {
      articleStatus: "published";
      publishedAt: Date;
    });

ここでは、記事IDとタイトルを ArticleBase にまとめ、ステータスごとに異なるプロパティを合併型で定義しています。

それぞれの型では、articleStatusにリテラル型を指定することで、記事の状態を区別しています。

  • articleStatusが "draft" の場合、publishedAt は設定できない
  • articleStatusが "published" の場合、publishedAt の設定が必須になる

このように、合併型とリテラル型を組み合わせることで、単に設定できる値を限定するだけでなく、記事の状態に応じて必要なデータの構造を表現できます。

publishedAt?: neverについて
下書きの記事では、公開日時を設定できないことを型で表現するために指定しています。never を使うことで、publishedAt に値を設定できないことを明示しています。

型設計で意識したいこと

ここまで、業務上のルールを型で表現する方法を見てきました。
実際に型設計を行う際には、業務ルールを型に反映するだけでなく、変更のしやすさや、型定義そのものの理解しやすさも考慮する必要があります。

例えば、記事のステータスが増えた場合、その状態に応じて必要なデータも変わるのであれば、型定義にも変更が必要になります。これは単なる保守コストではなく、新しい業務ルールをコードに反映するための変更でもあります。

また、共通するプロパティを別の型にまとめることで、重複を減らすこともできます。一方で、状態ごとに型を細かく分けるほど、型定義が複雑になる場合もあります。

大切なのは、型を短くすることでも、すべての制約を型だけで表現することでもありません。
業務上重要なルールを明確にし、理解しやすく変更しやすい形で表現することが重要です。

まとめ

本記事では、TypeScript の合併型とリテラル型について、基本的な使い方から実務での活用方法まで整理しました。

今回の内容を振り返ると、重要なポイントは次の3つです。

  • 合併型を使うことで、変数やプロパティに許容する型の範囲を定義できる
  • リテラル型を組み合わせることで、許容する値や状態を明示できる
  • 状態ごとに異なるデータ構造を合併型で表現することで、業務上の制約を型に反映できる

今回はあまり触れませんでしたが、実務では型を定義しただけですべての業務ルールを保証できるわけではありません。実行時のバリデーションやデータベース側の制約も含めて、どのようにデータの正しさを担保するかを考える必要があります。

型は単にエラーを減らすための仕組みではなく、コード上で業務ルールを表現し、設計意図を明確にするための手段でもあります。

普段の開発でも、単に型エラーを解消するだけでなく、

「この型は、どのようなデータを許容し、どのようなルールを表現しているのか?」

という視点を持って設計していきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?