TypeScriptプログラミングにおけるベストプラクティス

TypeScriptプログラミングにおけるベストプラクティス

この記事ではTypeScriptプログラミングにおけるベストプラクティスについて説明します。

TypeScriptの型を活かして、バグを減らし読みやすいコードを書くための実践的なベストプラクティスを解説します。

YouTube Video

TypeScriptプログラミングにおけるベストプラクティス

TypeScriptの最大の価値は「型によってバグを未然に防ぎ、コードの意図を明確にすること」です。

ベストプラクティスは単なるルールではなく、「安全で読みやすく、変更に強いコード」を書くための考え方の集合です。以下では、TypeScriptでよく使われるベストプラクティスを、実例を交えて紹介します。

any を避け、型は必ず意味を持たせる

最初に、「anyを避け、型に意味を持たせる」という点を見てみましょう。

any は型チェックを完全に無効化するため、TypeScriptを使う意味がほぼ失われます。「とりあえず動かす」ために any を使うのではなく、表現できる範囲で型を与えることが重要です。

1// Bad
2function parse(data: any) {
3  return data.value;
4}

このコードは何でも受け取れてしまい、実行時エラーを防げません。

1// Good
2type ParsedData = {
3  value: string;
4};
5
6function parse(data: ParsedData): string {
7  return data.value;
8}

型を定義することで、入力と出力の意図が明確になり、安全性が向上します。

オブジェクトの構造は type / interface で必ず明示する

続いて、「オブジェクトの構造は typeinterface で必ず明示する」という点を見てみましょう。

オブジェクトをその場限りで使うと、構造が曖昧になりやすくなります。再利用や変更を前提に、必ず型として切り出します。

1// Bad
2function createUser(user: { name: string; age: number }) {
3  console.log(user.name);
4}

小さなコードでも、型を分離する習慣が重要です。

1// Good
2type User = {
3  name: string;
4  age: number;
5};
6
7function createUser(user: User): void {
8  console.log(user.name);
9}

型に名前が付くことで、コード全体の理解が一気に楽になります。

Union型で「あり得る状態」を正確に表現する

条件分岐を stringnumber に任せると、想定外の値が紛れ込みます。Union型を使うことで、許可された状態だけを型レベルで表現できます。

1// Bad
2function setStatus(status: string) {
3  console.log(status);
4}

このコードでは、意味のない文字列や誤った値もコンパイル時に検出できません。

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4function setStatus(status: Status): void {
5  console.log(status);
6}

Union型を使うことで、「存在しない状態」をコンパイル時に確実に排除できます。結果として、条件分岐の安全性とコードの信頼性が向上します。

null / undefined は明示的に扱う

続いて、「nullundefined は明示的に扱う」という点を見てみましょう。

TypeScriptでは、「値が存在しない可能性」を型で表現することが重要です。曖昧なまま使うと、実行時エラーにつながります。

1type User = {
2  name: string;
3  email?: string;
4};

email は存在しない可能性があるため、その前提で処理します。

1function printEmail(user: User): void {
2  if (user.email) {
3    console.log(user.email);
4  }
5}

Optionalな値は、必ずチェックしてから使用します。

型アサーション(as)は最後の手段にする

続いて、「型アサーションを多用しない」という点を見てみましょう。

型アサーションは、TypeScriptの型チェックを一時的にすり抜けて、「この値はこの型だと分かっている」と宣言する行為です。多用すると型安全性が崩れます。

1// Bad
2const value = input as string;

このコードでは、実際の値が文字列でなくてもエラーにならず、実行時エラーの原因になります。次のコードのように、まずは型ガードで安全に確認する方法を選びます。

1// Good
2function isString(value: unknown): value is string {
3  return typeof value === "string";
4}
5
6if (isString(input)) {
7  console.log(input.toUpperCase());
8}

型ガードは、実際の値を確認しながら安全に型を確定させるための仕組みです。型アサーションより型ガードを優先することで、実行時エラーを防ぎやすくなります。

戻り値の型は推論に任せすぎない

続いて、「戻り値の型は推論に任せすぎない」という点を見てみましょう。

TypeScriptの型推論は強力ですが、公開関数では明示した方が安全です。将来の変更による影響を最小限にできます。

1// Bad
2function sum(a: number, b: number) {
3  return a + b;
4}

小さな関数でも、意図を明示します。

1// Good
2function sum(a: number, b: number): number {
3  return a + b;
4}

戻り値の型を書くことで、APIとしての安定性が高まります。

unknown を使って安全に受け取る

続いて、「unknown を使って外部入力を安全に受け取る」という点を見てみましょう。

APIやJSON、ユーザー入力などの外部入力には、any ではなく unknown を使います。これにより、値の検証を必ず行う設計になり、型安全性を保てます。

1// Bad
2function handleResponse(data: any) {
3  console.log(data.id);
4}

以下は、unknownを使って型を検証する方法です。

 1// Good
 2function handleResponse(data: unknown): void {
 3  if (
 4    typeof data === "object" &&
 5    data !== null &&
 6    "id" in data
 7  ) {
 8    console.log((data as { id: number }).id);
 9  }
10}

unknown は、そのままでは使えず、検証を前提とする型です。外部入力を扱う場面で特に力を発揮します。

小さな型を組み合わせて表現力を高める

続いて、「小さな型を組み合わせて表現力を高める」という点を見てみましょう。

巨大な型を一気に定義すると、可読性と保守性が下がります。意味のある単位で分割し、組み合わせます。

 1type Id = number;
 2
 3type UserProfile = {
 4  id: Id;
 5  name: string;
 6};
 7
 8type UserWithStatus = UserProfile & {
 9  status: "active" | "inactive";
10};

型を部品として扱うと、設計が整理されます。

typeinterface

interface の利点

typeinterface はどちらも型を定義できますが、用途と性質が異なります。役割に応じて使い分けることで、型定義の意図がより明確になります。

 1// Bad
 2type User = {
 3  id: number;
 4  name: string;
 5};
 6
 7type AdminUser = {
 8  id: number;
 9  name: string;
10  role: "admin";
11};

このように共通部分をコピーして増やしていくと、変更に弱くなります。

1// Good
2interface User {
3  id: number;
4  name: string;
5}
6
7interface AdminUser extends User {
8  role: "admin";
9}

interface は拡張(extends)を前提とした設計に向いており、オブジェクトの「形」を表す場合に適しています。

type の利点

一方で、type はより表現力が高く、Union型や交差型を扱うのに適しています。

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4type ApiResponse<T> =
5  | { status: "success"; data: T }
6  | { status: "error"; message: string };

type は「状態」や「選択肢」、「組み合わせ」を表現するのに向いています。

typeinterface の使い分けの目安

オブジェクトの構造や契約を表す場合は interface を、Union型や交差型、型演算など表現力が必要な場合は type を使うという目安を意識できます。

どちらを使っても動作は変わりませんが、「なぜこの型が存在するのか」が伝わる選択をすることが重要です。

型は「ドキュメント」として書く意識を持つ

最後に、「型をドキュメントとして書く」という点を見てみましょう。

良い型定義は、コメント以上に多くの情報を伝えます。「この型を見れば仕様が理解できる」状態を目指すことが重要です。

1type ApiError = {
2  code: number;
3  message: string;
4  retryable: boolean;
5};

このように、型定義が一種の仕様書のように機能する点が、TypeScriptの大きな強みです。

まとめ

TypeScriptのベストプラクティスは、「厳しくすること」ではありません。型を通して意図を明確にし、変更に強いコードを書くことが本質です。

日々の開発で小さなルールを積み重ねることで、「レビューが楽になる」、「バグが減る」、「将来の自分や他人が理解しやすくなる」といった長期的な効果が得られます。

まず 「型でどのように表現できるか?」 を考えると、TypeScriptらしい、質の高いコードにつながります。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video