TypeScript のベストプラクティス
文法を習得するのはほんの始まりに過ぎません。優れたTypeScriptを書くためには、一連のベストプラクティスに従う必要があります。このレッスンでは、実際のプロジェクトから得られた最も有益な知見と原則をまとめています。
1. 命名規則
(1) 型の命名
| カテゴリ | 仕様 | 例 |
|---|---|---|
| インターフェース | パスカルケース | UserService, ApiResponse |
| タイプエイリアス | パスカルケース | Status, EventHandler |
| 汎用パラメータ | 1文字またはパスカルケース | T, K, TItem |
| 列挙型 | パスカルケース(値は大文字の場合もある) | HttpStatus, COLOR_RED |
| 列挙型の要素 | パスカルケース | HttpStatus.Ok |
(2) ファイル名の付け方
| 種類 | 仕様 | 例 |
|---|---|---|
| 通常モジュール | キャメルケース | userService.ts |
| ファイルの種類 | パスカルケース | UserController.ts |
| 宣言ファイル | モジュールと同じ名前 | lodash.d.ts |
| テストファイル | モジュール名.test | userService.test.ts |
(3) 輸出仕様
TYPESCRIPT
// ✅ Recommendations——Prioritize naming and exporting
export function addUser(user: User): void { }
export class UserController { }
export type Status = "active" | "inactive";
// ⚠️ Use Default Export with Caution——It's easy to make mistakes when renaming files
export default class UserController { }
// ✅ Library/Framework recommended — both options are available
export class UserController { }
export default UserController;
2. タイプデザインの原則
(1) 原則 1:広さよりも正確さ
TYPESCRIPT
// ❌ Broad——Type information lost
function process(value: any): any { }
// ❌ Slightly better, but still too broad
function process(value: string | number): string | number { }
// ✅ Accurate——Generics Preserve Type Information
function process<T extends string | number>(value: T): T { }
(2) 原則 2:計算するほうが手書きするより良い
TYPESCRIPT
// ❌ Manually maintain two locations——Prone to desynchronization
interface User { id: number; name: string; email: string; }
type UserKeys = "id" | "name" | "email";
// ✅ Inference Based on Source Type——Automatic Synchronization
type UserKeys2 = keyof User; // "id" | "name" | "email"
type UserValues = User[keyof User]; // number | string
(3) 原則3:継承よりも合成
TYPESCRIPT
// ❌ Deep Inheritance——The Problem with Fragile Base Classes
class BaseEntity { id: number; }
class TimestampedEntity extends BaseEntity { createdAt: Date; }
class FullEntity extends TimestampedEntity { createdBy: string; }
// ✅ Type Combinations——Flexible and decoupled
type WithId = { id: number };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithAudit = { createdBy: string; updatedBy: string };
type FullEntity2 = WithId & WithTimestamps & WithAudit;
type SimpleEntity = WithId; // Customizable Combinations
▶ 例:「any」を型安全なものにリファクタリングする
TYPESCRIPT
// ❌ Before the refactoring——everywhere any,Zero Type Safety
function processRequest(req: any): any {
let user = req.body.user; // any
let result = validate(user); // any
return { status: 200, data: result };
}
// ✅ After the refactoring——Type safety at every step
interface User { id: number; name: string; email: string; }
interface Request2 { body: { user: User } }
interface ValidationResult { valid: boolean; errors?: string[] }
interface Response2<T> { status: number; data: T }
function processRequest2(req: Request2): Response2<ValidationResult> {
let user: User = req.body.user; // ✅ User Type
let result: ValidationResult = validate2(user); // ✅ ValidationResult
return { status: 200, data: result };
}
function validate2(user: User): ValidationResult {
if (!user.email.includes("@")) {
return { valid: false, errors: ["Invalid email address"] };
}
return { valid: true };
}
3. anyの代替案
(1) いずれかの代替案として不明
TYPESCRIPT
// ❌ any——Turn off type checking
function process(value: any) {
return value.toUpperCase(); // Do not check,May crash during runtime
}
// ✅ unknown——You must narrow it first before you can use it.
function process2(value: unknown) {
if (typeof value === "string") {
return value.toUpperCase(); // ✅ Safe Use After Narrowing
}
throw new Error("Expectations string Type");
}
(2) anyの代替としてのジェネリック
TYPESCRIPT
// ❌ any——Missing Type Information
function first(arr: any[]): any {
return arr[0];
}
// ✅ Generics——Preserve Type Information
function first2<T>(arr: T[]): T {
return arr[0];
}
(3) anyの代わりにユニオン型を使用する
TYPESCRIPT
// ❌ any
let value: any;
// ✅ Composite Types——Clearly list the possible types
let value2: string | number | boolean;
(4) any オブジェクトをインデックスシグネチャに置き換える
TYPESCRIPT
// ❌ any Object
let config: any = { host: "localhost" };
// ✅ Index Signature
let config2: Record<string, string | number> = { host: "localhost" };
4. DRYの原則(Don't Repeat Yourself)
(1) 重複を避けるために、keyof、typeof、およびユーティリティ型を使用する
TYPESCRIPT
const THEMES = {
light: { bg: "#fff", text: "#333" },
dark: { bg: "#1a1a1a", text: "#e0e0e0" }
} as const;
// Type Inference from Values——No duplicate definitions
type ThemeName = keyof typeof THEMES; // "light" | "dark"
type ThemeColors = typeof THEMES["light"]; // { readonly bg: "..."; readonly text: "..." }
function getTheme(name: ThemeName): ThemeColors {
return THEMES[name];
}
(2) マッピング型を用いた一括変換
TYPESCRIPT
interface ApiUser {
id: number;
name: string;
email: string;
role: string;
}
// No handwriting required——Derivation Using Tool Types
type CreateUserDTO = Omit<ApiUser, "id">;
type UpdateUserDTO = Partial<Omit<ApiUser, "id">>;
type UserSummary = Pick<ApiUser, "id" | "name">;
type UserResponse = Readonly<ApiUser>;
5. 型絞り込みの戦略
(1) できるだけ早い段階で絞り込む
TYPESCRIPT
// ❌ The gap is narrowing——Check each location before use
function process(value: string | number) {
console.log(value.toString()); // Only shared methods can be used
if (typeof value === "string") {
console.log(value.toUpperCase());
}
// Further checks will be needed later....
}
// ✅ Narrow as soon as possible——Use directly within the branch
function process2(value: string | number) {
if (typeof value === "string") {
// The entire branch is string
console.log(value.toUpperCase());
console.log(value.trim());
return;
}
// This must be number
console.log(value.toFixed(2));
}
(2) カスタム型ガードにおける絞り込みロジックの再利用
TYPESCRIPT
// The Complex Logic Behind Narrowing——Extract as a type guard
function isValidUser(obj: any): obj is User {
return obj
&& typeof obj.id === "number"
&& typeof obj.name === "string"
&& typeof obj.email === "string";
}
// Reuse in Multiple Places
function processUser(data: unknown) {
if (isValidUser(data)) {
console.log(data.name); // ✅ Type Safety
}
}
6. チームでの協働に関するガイドライン
(1) 統一された tsconfig 設定
JSON
{
"compilerOptions": {
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
同じルールがすべてのチームメンバーのIDEに自動的に適用されるため、手動での設定は不要です。
(2) ESLintのTypeScriptルール
JSON
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/explicit-function-return-type": "warn"
}
}
(3) コードレビューのチェックリスト
- [ ]
anyタイプではない(理由がコメントで説明されている場合を除く) - [ ] 関数の引数と戻り値には型注釈が付いています
- [ ] null または undefined である可能性のある値をチェックします
- [ ]
@ts-ignoreは使用されませんでした(@ts-expect-errorに置き換えられました) - [ ] 公開APIにはJSDocコメントが含まれています
- [ ] 型インポートでは
import typeを使用しています
❓ よくある質問
Q プロジェクト内で
any の使用を完全に禁止すべきでしょうか?A それは現実的ではありません。ESLint の設定を
error ではなく no-explicit-any: warn に設定してください。これにより、ごく少数の any の使用は許可されますが、レビューが必要であることを通知します。anyは、型指定のないサードパーティ製ライブラリ、複雑な型変換、迅速なプロトタイピングなどのシナリオでは妥当です。重要なのは、その理由を説明するコメントを付け、置き換えの計画を立てることです。Q 型定義は別のファイルに置くべきか、それとも使用されるファイル内に置くべきか?
A パブリック/共有の型は
types/ または models/ ディレクトリに配置すべきです。単一のファイル内でのみ使用される型は、そのファイル内に直接定義すべきです。ルール:3つ以上のファイルから参照される型は、パブリック型ファイルに抽出してください。それ以外の場合は、使用されているファイル内で定義してください。Q TypeScriptプロジェクトでESLintを使うべきですか?
A はい。tscは型チェックを担当し、ESLintはコード品質のチェックを担当します。この2つは互いに置き換えるものではなく、補完し合う関係にあります。
@typescript-eslintプラグインはTypeScript固有のルール(no-explicit-anyやconsistent-type-importsなど)を提供しており、TypeScriptプロジェクトにおける標準的な選択肢となっています。Q ジェネリック型パラメータが多すぎる場合はどうすればよいですか?
A 型パラメータが3つを超える場合は、以下の点を検討してください:(1) 複数のジェネリックをオブジェクトパラメータに置き換える;(2) 一部の型を別のインターフェースに抽出する;(3) ジェネリックのデフォルト値を使用して、指定が必要なパラメータの数を減らす。型パラメータが多すぎる場合は、通常、抽象化のレベルが不適切であることを示しています。
📖 まとめ
- 命名規則:インターフェース、型、クラスにはPascalCaseを使用し、ジェネリックには1文字またはPascalCaseを使用する
- タイプデザイン:汎用性よりも正確性、手作業によるコーディングよりも派生、継承よりも構成
anyの代替案:unknown(安全のためのフォールバック)、ジェネリック(型の保持)、ユニオン型(明示的な列挙)- DRY タイプ:
keyof、typeof、またはユーティリティ型を用いてソースから派生したもの。重複した定義はない。 - 型の絞り込み:できるだけ早い段階で絞り込みを行い、型ガードを再利用する
- チームガイドライン:標準化された tsconfig、ESLint ルール、およびコードレビューチェックリスト
📝 練習問題
- 基本演習(難易度 ⭐):自分が書いた、あるいは見たことのある
any型の例を見つけ、それをunknownまたはジェネリック型に置き換えてください。置き換え後も機能が同じままであり、コードの型安全性が向上していることを確認してください。 - 上級問題(難易度 ⭐⭐):DRYの原則に従って、一連の型定義をリファクタリングしてください。与えられた
ApiProductインターフェースに基づき、ユーティリティ型を使用して、CreateProduct、UpdateProduct、ProductSummary、ProductResponseの4つの型を導出してください。その際、重複するプロパティを手動で記述してはいけません。 - 課題(難易度:⭐⭐⭐):チーム向けのTypeScriptコーディングガイドラインを作成してください。これには、命名規則、
anyの代替案、型のインポート規則、推奨されるtsconfigの設定、および推奨されるESLintルールを含めてください。各ルールについて、その根拠と、具体例および反例を併せて提示してください。



