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 で必ず明示する
続いて、「オブジェクトの構造は typeやinterface で必ず明示する」という点を見てみましょう。
オブジェクトをその場限りで使うと、構造が曖昧になりやすくなります。再利用や変更を前提に、必ず型として切り出します。
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型で「あり得る状態」を正確に表現する
条件分岐を string や number に任せると、想定外の値が紛れ込みます。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 は明示的に扱う
続いて、「nullやundefined は明示的に扱う」という点を見てみましょう。
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};型を部品として扱うと、設計が整理されます。
type と interface
interface の利点
type と interface はどちらも型を定義できますが、用途と性質が異なります。役割に応じて使い分けることで、型定義の意図がより明確になります。
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 は「状態」や「選択肢」、「組み合わせ」を表現するのに向いています。
type と interface の使い分けの目安
オブジェクトの構造や契約を表す場合は interface を、Union型や交差型、型演算など表現力が必要な場合は type を使うという目安を意識できます。
どちらを使っても動作は変わりませんが、「なぜこの型が存在するのか」が伝わる選択をすることが重要です。
型は「ドキュメント」として書く意識を持つ
最後に、「型をドキュメントとして書く」という点を見てみましょう。
良い型定義は、コメント以上に多くの情報を伝えます。「この型を見れば仕様が理解できる」状態を目指すことが重要です。
1type ApiError = {
2 code: number;
3 message: string;
4 retryable: boolean;
5};このように、型定義が一種の仕様書のように機能する点が、TypeScriptの大きな強みです。
まとめ
TypeScriptのベストプラクティスは、「厳しくすること」ではありません。型を通して意図を明確にし、変更に強いコードを書くことが本質です。
日々の開発で小さなルールを積み重ねることで、「レビューが楽になる」、「バグが減る」、「将来の自分や他人が理解しやすくなる」といった長期的な効果が得られます。
まず 「型でどのように表現できるか?」 を考えると、TypeScriptらしい、質の高いコードにつながります。
YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。