TypeScriptのベストプラクティス:より安全で保守性の高いコードを書く
strictモード、ユーティリティ型、ジェネリクス、判別共用体、型システムの抜け穴を避ける方法など、最も重要なTypeScriptのパターンを学びましょう。
TypeScriptは、ほとんどのプロフェッショナルなJavaScriptプロジェクトにおいて、オプションから必須へと変わりました。しかし、「一応TypeScript」と「実際にバグを検出するTypeScript」の間には大きな隔たりがあります。その差は、型システムの真の力を引き出すいくつかのパターンにかかっています。
strictモードを必ず有効にする
TypeScript設定で最も効果的な変更はこれです:
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strictは複数のチェックをまとめて有効にします:
strictNullChecks—nullとundefinedを他の型に代入できなくなるnoImplicitAny— 変数が暗黙的にany型を持てなくなるstrictFunctionTypes— 関数パラメータのチェックが厳格になるstrictPropertyInitialization— クラスプロパティは必ず初期化が必要になる
strictNullChecksがなければ、TypeScriptは最も一般的な実行時エラーの多くを見逃します。最初から有効にしましょう — 大規模なコードベースに後から適用するのは非常に大変です。
anyを避け、代わりにunknownを使う
anyは型システムの抜け穴であり、すべてのエラーを黙らせ、TypeScriptの意味をなくしてしまいます:
// ❌ anyはすべての型チェックを無効にする
function processData(data: any) {
data.nonExistentMethod(); // エラーなし!実行時にクラッシュする。
}
// ✅ unknownは使用前に型を絞り込むことを強制する
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // 安全 — stringに絞り込まれている
}
if (data instanceof Error) {
console.log(data.message); // 安全 — Errorに絞り込まれている
}
}
unknownはanyの型安全版です。使用する前に何を持っているかを確認することを強制します。APIレスポンス、JSONパース、外部ソースからのデータに使いましょう。
網羅的な処理のための判別共用体
TypeScriptの最も強力なパターンの一つです:
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // ここでstate.dataが利用可能
case "error":
return <ErrorMessage msg={state.message} />; // ここでstate.messageが利用可能
default:
// 新しいstateを追加してもここを処理しなければTypeScriptがエラーを出す
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
最後のnever代入は網羅性チェックです — ApiStateに新しいバリアントを追加してもswitch文でそれを処理しなければ、TypeScriptはコンパイル時にエラーを出します。これは言語全体の中で最も価値あるセーフティネットの一つです。
ユーティリティ型
TypeScriptには組み込みの型変換ツールが付属しています:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// すべてのプロパティをオプションにする
type PartialUser = Partial<User>;
// すべてのプロパティを必須にする
type RequiredUser = Required<User>;
// すべてのプロパティを読み取り専用にする
type ReadonlyUser = Readonly<User>;
// 特定のプロパティを選択する
type UserPreview = Pick<User, "id" | "name">;
// 特定のプロパティを除外する
type UserWithoutDates = Omit<User, "createdAt">;
// オブジェクトの値から型を作成する
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// プロパティをnullableにする
type NullableUser = { [K in keyof User]: User[K] | null };
// Record型:特定のキーと値の型を持つオブジェクト
type RolePermissions = Record<UserRole, string[]>;
// ユニオン型からExtract/Excludeする
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
再利用可能で型安全なコードのためのジェネリクス
ジェネリクスを使うと、あらゆる型で動作しながら完全な型安全性を保つ関数やクラスを書けます:
// ジェネリクスなし — 型情報が失われる
function first(arr: any[]): any {
return arr[0];
}
// ジェネリクスあり — 型を保持する
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // 型: number | undefined
const str = first(["a", "b"]); // 型: string | undefined
ジェネリクスの制約
// Tは'id'プロパティを持つ必要がある
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// KはTのキーである必要がある
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // 型: string
const role = getProperty(user, "role"); // 型: string
// getProperty(user, "missing"); // ❌ TypeScriptエラー — "missing"はキーではない
型ガードと型の絞り込み
// 型述語 — カスタム型ガード
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// 使用例
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScriptはここでdataがUserであることを認識する
}
実行時のAPIレスポンス検証には、TypeScriptのインターフェースとZodによる実行時スキーマバリデーションを組み合わせましょう。JSON to Zodコンバーターを使えば、JSONからZodスキーマを直接生成できます — サンプルのJSONレスポンスを貼り付けるだけで、即座に型安全なスキーマが得られます。
テンプレートリテラル型
TypeScript 4.1以降では、型レベルでの文字列操作がサポートされています:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
satisfies演算子(TypeScript 4.9以降)
値が型に一致するかを検証しつつ、型を広げません:
type ColorMap = Record<string, string>;
// ❌ satisfiesなし — 型がRecord<string, string>に広がる
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // 動くが文字列型の情報が失われる
// ✅ satisfiesあり — 検証しつつリテラル型を保持する
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScriptはstringであることを認識
colors.nonexistent; // ❌ エラー — キーが存在しない
データから型を生成する
APIレスポンスからTypeScriptのインターフェースを手動で書く代わりに:
- サンプルのJSONレスポンスをJSON to TypeScriptコンバーターに貼り付けて、インターフェースを即座に取得する
- またはJSON to ZodでZodスキーマを生成する — Zodは
z.infer<typeof schema>で自動的にTypeScriptの型を推論する
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// 型は自動的に推論される — 重複なし
type User = z.infer<typeof UserSchema>;
// 実行時に検証しつつTypeScriptの型も提供する
const user = UserSchema.parse(await response.json());
避けるべきアンチパターン
| アンチパターン | 問題点 | 解決策 |
|---|---|---|
as any |
型チェックを無効にする | unknown + 型の絞り込みを使う |
// @ts-ignore |
修正せずにエラーを黙らせる | 型エラーを修正する |
as SomeType(危険なキャスト) |
型推論を回避する | 型ガードを使う |
ユニオンにinterfaceを使う |
判別共用体で機能しない | typeを使う |
過度に汎用的なobject |
構造がない | 適切なインターフェースを定義する |
型としてのFunction |
パラメータ/戻り値の情報がない | 具体的なシグネチャを使う |
知っておくべきTypeScript設定
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n]がT | undefinedを返す
"exactOptionalPropertyTypes": true, // 欠落とundefinedを区別する
"noImplicitReturns": true, // すべてのコードパスが値を返す必要がある
"noFallthroughCasesInSwitch": true, // 意図しないswitchのフォールスルーを禁止
"verbatimModuleSyntax": true // 型のみのimportにimport typeを明示する
}
}
TypeScriptの価値は、どれだけ厳格に使うかに比例します。ここで紹介したパターン — strictモード、判別共用体、ジェネリクス、網羅性チェック — こそが、バグを発見するTypeScriptと、単に構文を追加するだけのTypeScriptを分けるものです。