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

関数の定義

関数を定義する際の重要なポイントを学びましょう。TypeScriptでは型注釈により、関数の入出力がより明確になります。

学習のポイント

  • 関数名は「動詞 + 名詞」で命名する
  • 単一責任の原則を守る
  • 引数は少なくする
  • DRY(Don't Repeat Yourself)原則を守る
  • 純粋関数を心がける
  • 副作用を避ける

関数は小さく作る

関数は1つの機能のみを持つようにしましょう。複数の機能を持つ関数は分割することで、可読性とテストのしやすさが向上します。

Bad

❌ Bad: 複数の処理が1つの関数に混在
// ❌ 複数の処理が1つの関数に混在
function processUserData(user: User): void {
// ユーザーの検証
if (!user.name || !user.email) {
throw new Error('Invalid user data');
}

// メールアドレスの正規化
const normalizedEmail = user.email.toLowerCase().trim();

// データベースに保存
database.save({ ...user, email: normalizedEmail });

// 確認メールを送信
emailService.send(normalizedEmail, 'Welcome!');

// ログを記録
logger.info(`User ${user.name} created`);
}

Good

✅ Good: 単一責任の原則に従って分割
// ✅ 単一責任の原則に従って分割
function validateUser(user: User): void {
if (!user.name || !user.email) {
throw new Error('Invalid user data');
}
}

function normalizeEmail(email: string): string {
return email.toLowerCase().trim();
}

function saveUser(user: User): void {
database.save(user);
}

function sendWelcomeEmail(email: string): void {
emailService.send(email, 'Welcome!');
}

function logUserCreation(userName: string): void {
logger.info(`User ${userName} created`);
}

// メイン処理
function createUser(user: User): void {
validateUser(user);
const normalizedUser = { ...user, email: normalizeEmail(user.email) };
saveUser(normalizedUser);
sendWelcomeEmail(normalizedUser.email);
logUserCreation(user.name);
}

関数名は動詞 + 名詞で命名する

関数名は「何をするか」が明確にわかる名前にしましょう。

Bad

❌ Bad: 何をする関数かわからない
// ❌ 何をする関数かわからない
function user(id: number): User | null { /* ... */ }
function data(): string[] { /* ... */ }
function process(items: Item[]): void { /* ... */ }

Good

✅ Good: 動詞 + 名詞で明確に
// ✅ 動詞 + 名詞で明確に
function getUserById(id: number): User | null { /* ... */ }
function fetchDataFromApi(): string[] { /* ... */ }
function filterActiveItems(items: Item[]): Item[] { /* ... */ }

// よく使う動詞のパターン
function createUser(data: UserData): User { /* ... */ } // 作成
function updateUser(user: User): void { /* ... */ } // 更新
function deleteUser(id: number): void { /* ... */ } // 削除
function findUserByEmail(email: string): User | null { /* ... */ } // 検索
function validateEmail(email: string): boolean { /* ... */ } // 検証
function calculateTotal(items: Item[]): number { /* ... */ } // 計算
function formatDate(date: Date): string { /* ... */ } // 整形
function parseJson(json: string): object { /* ... */ } // 解析

真偽値を返す関数は is/has/can で始める

✅ Good: 真偽値を返す関数の命名パターン
// ✅ 真偽値を返す関数の命名パターン
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}

function hasPermission(user: User, action: string): boolean {
return user.permissions.includes(action);
}

function canAccessResource(user: User, resourceId: string): boolean {
return user.accessibleResources.includes(resourceId);
}

function shouldRetry(error: Error, attempts: number): boolean {
return error.name === 'NetworkError' && attempts < 3;
}

引数は少なくする

引数が多い関数は理解しづらく、呼び出し時にミスが起きやすくなります。3つ以上の引数はオブジェクトにまとめましょう。

Bad

❌ Bad: 引数が多すぎる
// ❌ 引数が多すぎる
function createUser(
name: string,
email: string,
age: number,
address: string,
phone: string,
isAdmin: boolean
): User {
return { name, email, age, address, phone, isAdmin };
}

// 呼び出し側:順番を間違えやすい
const user = createUser('Taro', 'taro@example.com', 25, 'Tokyo', '090-1234-5678', false);

Good

✅ Good: オブジェクトでまとめる
// ✅ オブジェクトでまとめる
interface CreateUserParams {
name: string;
email: string;
age: number;
address: string;
phone: string;
isAdmin?: boolean; // オプショナルパラメータ
}

function createUser(params: CreateUserParams): User {
return {
...params,
isAdmin: params.isAdmin ?? false, // デフォルト値
};
}

// 呼び出し側:プロパティ名で明確
const user = createUser({
name: 'Taro',
email: 'taro@example.com',
age: 25,
address: 'Tokyo',
phone: '090-1234-5678',
});

デフォルト引数を活用する

TypeScriptではデフォルト引数を使うことで、オプショナルなパラメータを簡潔に扱えます。

✅ Good: デフォルト引数の活用
// ✅ デフォルト引数の活用
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}

greet('Taro'); // 'Hello, Taro!'
greet('Taro', 'Hi'); // 'Hi, Taro!'

// オブジェクト引数でのデフォルト値
interface PaginationOptions {
page?: number;
limit?: number;
sortBy?: string;
order?: 'asc' | 'desc';
}

function fetchItems(options: PaginationOptions = {}): Promise<Item[]> {
const {
page = 1,
limit = 10,
sortBy = 'createdAt',
order = 'desc',
} = options;

return api.get('/items', { page, limit, sortBy, order });
}

DRY(Don't Repeat Yourself)原則

同じコードを繰り返さないようにしましょう。重複したコードは関数として抽出します。

Bad

❌ Bad: 同じ処理が重複
// ❌ 同じ処理が重複
function calculateCircleArea(radius: number): number {
return 3.14159 * radius * radius;
}

function calculateCircleCircumference(radius: number): number {
return 2 * 3.14159 * radius;
}

function calculateSphereVolume(radius: number): number {
return (4 / 3) * 3.14159 * radius * radius * radius;
}

Good

✅ Good: 定数を共通化
// ✅ 定数を共通化
const PI = Math.PI;

function calculateCircleArea(radius: number): number {
return PI * radius ** 2;
}

function calculateCircleCircumference(radius: number): number {
return 2 * PI * radius;
}

function calculateSphereVolume(radius: number): number {
return (4 / 3) * PI * radius ** 3;
}

Bad

❌ Bad: バリデーションロジックが散在
// ❌ バリデーションロジックが散在
function createUser(userData: UserData): User {
if (!userData.email.includes('@')) {
throw new Error('Invalid email');
}
// ...
}

function updateUser(userData: UserData): User {
if (!userData.email.includes('@')) {
throw new Error('Invalid email');
}
// ...
}

Good

✅ Good: バリデーションを共通化
// ✅ バリデーションを共通化
function validateEmail(email: string): void {
if (!email.includes('@')) {
throw new Error('Invalid email');
}
}

function createUser(userData: UserData): User {
validateEmail(userData.email);
// ...
}

function updateUser(userData: UserData): User {
validateEmail(userData.email);
// ...
}

純粋関数を心がける

純粋関数とは、同じ入力に対して常に同じ出力を返し、副作用を持たない関数です。

Bad

❌ Bad: 副作用がある(外部の状態を変更)
// ❌ 副作用がある(外部の状態を変更)
let total = 0;

function addToTotal(value: number): number {
total += value; // 外部変数を変更
return total;
}

// ❌ 外部状態に依存(同じ入力でも結果が変わる)
let discount = 0.1;

function calculatePrice(price: number): number {
return price * (1 - discount); // 外部変数に依存
}

Good

✅ Good: 純粋関数(外部に依存しない、副作用なし)
// ✅ 純粋関数(外部に依存しない、副作用なし)
function add(a: number, b: number): number {
return a + b;
}

function calculatePrice(price: number, discountRate: number): number {
return price * (1 - discountRate);
}

// ✅ 配列操作も新しい配列を返す
function addItem<T>(items: T[], newItem: T): T[] {
return [...items, newItem]; // 元の配列を変更しない
}

function removeItem<T>(items: T[], index: number): T[] {
return items.filter((_, i) => i !== index);
}

副作用を分離する

副作用(I/O操作、状態変更など)は避けられない場合もありますが、純粋な計算処理と分離しましょう。

Bad

❌ Bad: 計算と副作用が混在
// ❌ 計算と副作用が混在
function processOrder(order: Order): void {
// 計算
const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;

// 副作用
database.save({ ...order, total });
emailService.sendReceipt(order.email, total);
logger.info(`Order processed: ${total}`);
}

Good

✅ Good: 純粋な計算処理
// ✅ 純粋な計算処理
function calculateOrderTotal(items: OrderItem[]): {
subtotal: number;
tax: number;
total: number;
} {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;
return { subtotal, tax, total };
}

// ✅ 副作用を明示的に分離
async function saveOrder(order: Order, total: number): Promise<void> {
await database.save({ ...order, total });
}

async function notifyOrderComplete(email: string, total: number): Promise<void> {
await emailService.sendReceipt(email, total);
}

// ✅ オーケストレーション
async function processOrder(order: Order): Promise<void> {
const { total } = calculateOrderTotal(order.items);
await saveOrder(order, total);
await notifyOrderComplete(order.email, total);
logger.info(`Order processed: ${total}`);
}

参照透過性を保つ

参照透過性とは、式をその値で置き換えても、プログラムの動作が変わらない性質のことです。

Bad

❌ Bad: 参照透過性がない(呼び出しごとに結果が変わる)
// ❌ 参照透過性がない(呼び出しごとに結果が変わる)
function getCurrentTime(): string {
return new Date().toISOString();
}

function getRandomId(): number {
return Math.random();
}

Good

✅ Good: 参照透過性がある(依存を引数で注入)
// ✅ 参照透過性がある(依存を引数で注入)
function formatTime(date: Date): string {
return date.toISOString();
}

function generateId(seed: number): number {
// 決定論的なID生成
return seed * 2654435761 % 2 ** 32;
}

// テスト可能になる
const testDate = new Date('2024-01-01');
console.log(formatTime(testDate)); // 常に同じ結果

早期リターンを使う

条件が満たされない場合は早期にリターンすることで、ネストを減らし可読性を上げます。

Bad

❌ Bad: ネストが深い
// ❌ ネストが深い
function processUser(user: User | null): string {
if (user !== null) {
if (user.isActive) {
if (user.hasPermission) {
return `Welcome, ${user.name}!`;
} else {
return 'Permission denied';
}
} else {
return 'Account is inactive';
}
} else {
return 'User not found';
}
}

Good

✅ Good: 早期リターンでフラットに
// ✅ 早期リターンでフラットに
function processUser(user: User | null): string {
if (user === null) {
return 'User not found';
}

if (!user.isActive) {
return 'Account is inactive';
}

if (!user.hasPermission) {
return 'Permission denied';
}

return `Welcome, ${user.name}!`;
}

型ガードを活用する

TypeScriptの型ガードを使って、型を絞り込みましょう。

// カスタム型ガード
interface Dog {
type: 'dog';
bark(): void;
}

interface Cat {
type: 'cat';
meow(): void;
}

type Pet = Dog | Cat;

function isDog(pet: Pet): pet is Dog {
return pet.type === 'dog';
}

function playWithPet(pet: Pet): void {
if (isDog(pet)) {
pet.bark(); // TypeScriptはここでDog型と認識
} else {
pet.meow(); // TypeScriptはここでCat型と認識
}
}

// nullチェックの型ガード
function assertExists<T>(value: T | null | undefined, message?: string): asserts value is T {
if (value === null || value === undefined) {
throw new Error(message ?? 'Value does not exist');
}
}

function processUser(user: User | null): void {
assertExists(user, 'User not found');
// ここ以降、userはUser型として扱える
console.log(user.name);
}

オーバーロードを活用する

TypeScriptの関数オーバーロードで、異なる引数パターンに対応できます。

// 関数オーバーロード
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}

const div = createElement('div'); // HTMLDivElement
const span = createElement('span'); // HTMLSpanElement
const input = createElement('input'); // HTMLInputElement

// ジェネリクスと組み合わせる
function parseValue(value: string, type: 'number'): number;
function parseValue(value: string, type: 'boolean'): boolean;
function parseValue(value: string, type: 'string'): string;
function parseValue(value: string, type: string): number | boolean | string {
switch (type) {
case 'number':
return Number(value);
case 'boolean':
return value === 'true';
default:
return value;
}
}

練習問題

以下のコードをクリーンコードの原則に従ってリファクタリングしてください
// 問題:以下のコードを改善してください
function calc(a: number, b: number, c: number, d: string): number | string {
if (d === 'add') {
let result = 0;
result = a + b + c;
return result;
} else if (d === 'multiply') {
let result = 1;
result = a * b * c;
return result;
} else if (d === 'average') {
let result = 0;
result = (a + b + c) / 3;
return result;
} else {
return 'Unknown operation';
}
}

解答例:

type MathOperation = 'add' | 'multiply' | 'average';

function add(...numbers: number[]): number {
return numbers.reduce((sum, num) => sum + num, 0);
}

function multiply(...numbers: number[]): number {
return numbers.reduce((product, num) => product * num, 1);
}

function average(...numbers: number[]): number {
if (numbers.length === 0) return 0;
return add(...numbers) / numbers.length;
}

function calculate(numbers: number[], operation: MathOperation): number {
const operations: Record<MathOperation, (...nums: number[]) => number> = {
add,
multiply,
average,
};

return operations[operation](...numbers);
}

// 使用例
const result = calculate([1, 2, 3], 'add'); // 6

改善ポイント:

  1. 操作を型で制限(リテラル型)
  2. 各計算を個別の純粋関数に分離
  3. 可変長引数で柔軟性を確保
  4. オブジェクトでディスパッチ(if/else を排除)
  5. 無効な操作は型で防止

関数定義のまとめ

関数定義のベストプラクティス
  • 関数は小さく、単一責任で作る
  • 関数名は動詞 + 名詞で命名
  • 引数は 3 つ以下、多い場合はオブジェクト化
  • デフォルト引数を活用する
  • DRY 原則で重複を排除する
  • 純粋関数を心がける
  • 副作用は分離する
  • 早期リターンでネストを減らす
  • 型ガードで型を絞り込む