メインコンテンツまでスキップ

TypeScript 特有のベストプラクティス

TypeScriptの型システムを最大限に活用するためのベストプラクティスを学びましょう。

学習のポイント

  • any を避け、適切な型を使用する
  • unknown と never を理解する
  • ジェネリクスを活用する
  • ユーティリティ型を使いこなす
  • 型の絞り込みを行う

any を避ける

any は型チェックを無効にするため、TypeScriptの恩恵を受けられなくなります。

Bad

❌ Bad: any を使用
// ❌ any を使用
function processData(data: any): any {
return data.value.toUpperCase();
}

const result = processData({ value: 123 }); // ランタイムエラー

Good

✅ Good: 適切な型を定義
// ✅ 適切な型を定義
interface DataWithValue {
value: string;
}

function processData(data: DataWithValue): string {
return data.value.toUpperCase();
}

const result = processData({ value: 'hello' }); // 'HELLO'
// processData({ value: 123 }); // コンパイルエラー

unknown を使う

型が不明な場合は any ではなく unknown を使いましょう。

✅ Good: unknown で受け取り、型を確認してから使用
// ✅ unknown で受け取り、型を確認してから使用
function parseJson(json: string): unknown {
return JSON.parse(json);
}

const data = parseJson('{"name": "Taro"}');

// data.name; // エラー: 'unknown' 型にはプロパティがない

// 型ガードで絞り込む
function isUser(value: unknown): value is { name: string } {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
typeof (value as { name: unknown }).name === 'string'
);
}

if (isUser(data)) {
console.log(data.name); // OK
}

never を理解する

never は絶対に発生しない値の型です。到達不能なコードや網羅性チェックに使用します。

// 網羅性チェック
type Color = 'red' | 'green' | 'blue';

function getColorCode(color: Color): string {
switch (color) {
case 'red':
return '#ff0000';
case 'green':
return '#00ff00';
case 'blue':
return '#0000ff';
default:
// 全てのケースを処理していれば、ここには到達しない
const exhaustiveCheck: never = color;
throw new Error(`Unknown color: ${exhaustiveCheck}`);
}
}

// 例外をスローする関数の戻り値
function throwError(message: string): never {
throw new Error(message);
}

ジェネリクスを活用する

ジェネリクスを使うことで、型安全で再利用可能なコードを書けます。

✅ Good: ジェネリクスで汎用的な関数
// ✅ ジェネリクスで汎用的な関数
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}

const firstNumber = getFirst([1, 2, 3]); // number | undefined
const firstString = getFirst(['a', 'b']); // string | undefined

// ✅ 制約付きジェネリクス
interface HasId {
id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}

interface User extends HasId {
name: string;
}

const users: User[] = [{ id: 1, name: 'Taro' }];
const user = findById(users, 1); // User | undefined

// ✅ 複数の型パラメータ
function map<T, U>(items: T[], fn: (item: T) => U): U[] {
return items.map(fn);
}

const lengths = map(['hello', 'world'], s => s.length); // number[]

ユーティリティ型を使いこなす

TypeScriptの組み込みユーティリティ型を活用しましょう。

interface User {
id: number;
name: string;
email: string;
age: number;
}

// Partial: 全てのプロパティをオプショナルに
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }

// Required: 全てのプロパティを必須に
type RequiredUser = Required<PartialUser>;

// Pick: 特定のプロパティのみ抽出
type UserSummary = Pick<User, 'id' | 'name'>;
// { id: number; name: string }

// Omit: 特定のプロパティを除外
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; age: number }

// Record: キーと値の型を指定したオブジェクト型
type UserById = Record<number, User>;

// Readonly: 全てのプロパティを読み取り専用に
type ReadonlyUser = Readonly<User>;

// ReturnType: 関数の戻り値の型を取得
function createUser(): User {
return { id: 1, name: 'Taro', email: 'taro@example.com', age: 25 };
}
type CreatedUser = ReturnType<typeof createUser>; // User

// Parameters: 関数のパラメータの型をタプルとして取得
type CreateUserParams = Parameters<typeof createUser>; // []

型の絞り込み(Type Narrowing)

条件分岐で型を絞り込むことで、より安全なコードを書けます。

// typeof による絞り込み
function process(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase(); // string として扱える
}
return value.toFixed(2); // number として扱える
}

// in 演算子による絞り込み
interface Dog {
bark(): void;
}

interface Cat {
meow(): void;
}

function makeSound(animal: Dog | Cat): void {
if ('bark' in animal) {
animal.bark(); // Dog として扱える
} else {
animal.meow(); // Cat として扱える
}
}

// instanceof による絞り込み
function formatError(error: unknown): string {
if (error instanceof Error) {
return error.message; // Error として扱える
}
return String(error);
}

// カスタム型ガード
interface ApiResponse<T> {
success: true;
data: T;
}

interface ApiError {
success: false;
error: string;
}

type ApiResult<T> = ApiResponse<T> | ApiError;

function isSuccess<T>(result: ApiResult<T>): result is ApiResponse<T> {
return result.success === true;
}

function handleResult<T>(result: ApiResult<T>): T | null {
if (isSuccess(result)) {
return result.data; // ApiResponse<T> として扱える
}
console.error(result.error); // ApiError として扱える
return null;
}

const assertion

as const を使うことで、リテラル型として推論させることができます。

// as const なし
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
// 型: { apiUrl: string; timeout: number }

// as const あり
const configConst = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const;
// 型: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }

// 配列での使用
const colors = ['red', 'green', 'blue'] as const;
// 型: readonly ["red", "green", "blue"]

type Color = typeof colors[number]; // "red" | "green" | "blue"

テンプレートリテラル型

文字列リテラル型を組み合わせて新しい型を作成できます。

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';

// テンプレートリテラル型
type ApiRoute = `${HttpMethod} ${Endpoint}`;
// "GET /users" | "GET /posts" | "GET /comments" | "POST /users" | ...

// イベント名の型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"
type ChangeEvent = EventName<'change'>; // "onChange"

練習問題

以下のコードをTypeScriptの型機能を活用してリファクタリングしてください
// 問題
function fetchData(url: any): any {
// APIからデータを取得
return fetch(url).then(res => res.json());
}

function processItems(items: any[]): any[] {
return items.map(item => ({
id: item.id,
name: item.name.toUpperCase(),
}));
}

解答:

✅ Good: リファクタリング後
// ✅ リファクタリング後
interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: string;
}

async function fetchData<T>(url: string, options?: FetchOptions): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}

interface RawItem {
id: number;
name: string;
}

interface ProcessedItem {
id: number;
name: string;
}

function processItems(items: RawItem[]): ProcessedItem[] {
return items.map(item => ({
id: item.id,
name: item.name.toUpperCase(),
}));
}

// 使用例
interface User {
id: number;
name: string;
email: string;
}

async function getUsers(): Promise<User[]> {
return fetchData<User[]>('/api/users');
}

改善ポイント:

  1. any を排除し、具体的な型を定義
  2. ジェネリクスで再利用可能な fetchData
  3. インターフェースで入出力を明確化
  4. async/await でエラーハンドリングを追加

TypeScript特有のベストプラクティスまとめ

TypeScript 特有のベストプラクティス
  • any を避け、unknown を使用する
  • 適切な型定義を行う
  • ジェネリクスで再利用可能なコードを書く
  • ユーティリティ型を活用する
  • 型の絞り込みで安全性を高める
  • as const でリテラル型を活用
  • 網羅性チェックで漏れを防ぐ