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

関数の応用

この章では、TypeScriptにおける関数のより高度な機能について学びます。

この章で学ぶこと

  • 関数のオーバーロード
  • コールバック関数の型付け
  • レストパラメータの使い方
  • thisの型付け
備考

この章のコード例は、TypeScript Playgroundで試すことができます。

関数のオーバーロード

関数のオーバーロードとは、同じ名前の関数で異なる型のパラメータや戻り値を扱う機能です。TypeScriptでは、複数の「オーバーロードシグネチャ」と1つの「実装シグネチャ」を定義します。

オーバーロードの基本

// オーバーロードシグネチャ(宣言だけ)
function reverse(value: string): string;
function reverse(value: number): number;

// 実装シグネチャ(実際の実装)
function reverse(value: string | number): string | number {
if (typeof value === 'string') {
return value.split('').reverse().join('');
} else {
return Number(value.toString().split('').reverse().join(''));
}
}

// 使用例
console.log(reverse('hello')); // 'olleh' (string型を返す)
console.log(reverse(12345)); // 54321 (number型を返す)

コード解説:

  • オーバーロードシグネチャ: 関数の異なる呼び出し方を宣言
  • 実装シグネチャ: すべてのオーバーロードを満たす実際の実装
  • TypeScriptは呼び出し時に適切なオーバーロードを選択

より複雑なオーバーロード

引数の数が異なるオーバーロードも可能です。

// 引数の数が異なるオーバーロード
function createElement(tag: string): HTMLElement;
function createElement(tag: string, content: string): HTMLElement;
function createElement(tag: string, content: string, className: string): HTMLElement;

function createElement(
tag: string,
content?: string,
className?: string
): HTMLElement {
const element = document.createElement(tag);

if (content) {
element.textContent = content;
}

if (className) {
element.className = className;
}

return element;
}

// 使用例
const div1 = createElement('div');
const div2 = createElement('div', 'Hello');
const div3 = createElement('div', 'Hello', 'container');

実践的なオーバーロード例

// ユーザーを検索する関数: IDまたは名前で検索可能
type User = { id: number; name: string; email: string };

function findUser(id: number): User | undefined;
function findUser(name: string): User | undefined;

function findUser(idOrName: number | string): User | undefined {
// ダミーデータ
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
];

if (typeof idOrName === 'number') {
return users.find((user) => user.id === idOrName);
} else {
return users.find((user) => user.name === idOrName);
}
}

// 使用例
console.log(findUser(1)); // { id: 1, name: 'Alice', ... }
console.log(findUser('Bob')); // { id: 2, name: 'Bob', ... }

オーバーロードの注意点

// 実装シグネチャはオーバーロードシグネチャを包含する必要がある
function add(a: number, b: number): number;
function add(a: string, b: string): string;

// 実装シグネチャは両方の型をサポートする必要がある
function add(a: number | string, b: number | string): number | string {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else {
return String(a) + String(b);
}
}

console.log(add(5, 3)); // 8
console.log(add('Hello, ', 'World')); // 'Hello, World'
警告

2026年の潮流: オーバーロードより先にジェネリクス制約を検討する

関数オーバーロードは型推論の仕組みが複雑で、実装と型定義が乖離しやすいという欠点があります。以下のようなケースでは、オーバーロードを使わずにジェネリクス制約で同じ型安全性を実現できます。

// ❌ オーバーロードで書く場合
function firstOrDefault(arr: string[], defaultValue: string): string;
function firstOrDefault(arr: number[], defaultValue: number): number;
function firstOrDefault<T>(arr: T[], defaultValue: T): T {
return arr[0] ?? defaultValue
}

// ✅ ジェネリクス制約で書けば宣言も短い
function firstOrDefault<T>(arr: T[], defaultValue: T): T {
return arr[0] ?? defaultValue
}

オーバーロードが必要になるのは、引数の数や組み合わせで戻り値の型が大きく変わる場合(例: DOM の querySelector がタグ名で型を変える)です。単に引数の型バリエーションがあるだけなら、ジェネリクスや conditional types で表現するほうが保守性が高くなります。

コールバック関数

コールバック関数とは、他の関数に引数として渡される関数のことです。非同期処理やイベント処理でよく使用されます。

コールバック関数の型付け

// コールバック関数を受け取る関数の型定義
function processNumbers(
numbers: number[],
callback: (num: number) => number
): number[] {
return numbers.map(callback);
}

// 使用例
const doubled = processNumbers([1, 2, 3, 4, 5], (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

const squared = processNumbers([1, 2, 3, 4, 5], (n) => n * n);
console.log(squared); // [1, 4, 9, 16, 25]

コード解説:

  • callback: (num: number) => number: コールバック関数の型
  • (num: number): 引数としてnumber型を1つ受け取る
  • => number: 戻り値としてnumber型を返す

型エイリアスを使ったコールバック定義

// コールバック関数の型を型エイリアスで定義
type NumberProcessor = (num: number) => number;
type StringProcessor = (str: string) => string;

function processItems<T>(
items: T[],
processor: (item: T) => T
): T[] {
return items.map(processor);
}

// number配列の処理
const numbers = processItems([1, 2, 3], (n) => n * 2);
console.log(numbers); // [2, 4, 6]

// string配列の処理
const strings = processItems(['a', 'b', 'c'], (s) => s.toUpperCase());
console.log(strings); // ['A', 'B', 'C']

非同期コールバック

// 成功/失敗のコールバックを受け取る関数
function fetchData(
url: string,
onSuccess: (data: unknown) => void,
onError: (error: Error) => void
): void {
fetch(url)
.then((response) => response.json())
.then((data) => onSuccess(data))
.catch((error) => onError(error));
}

// 使用例
fetchData(
'https://api.example.com/data',
(data) => {
console.log('Success:', data);
},
(error) => {
console.error('Error:', error.message);
}
);

イベントハンドラのコールバック

// イベントコールバックの型定義
type EventCallback = (event: Event) => void;

function addClickListener(
element: HTMLElement,
callback: EventCallback
): void {
element.addEventListener('click', callback);
}

// 使用例
const button = document.getElementById('myButton') as HTMLElement;
addClickListener(button, (event) => {
console.log('Button clicked', event);
});

複数のコールバックを持つ関数

// 処理の各段階でコールバックを呼び出す関数
type ProgressCallback = (progress: number) => void;
type CompleteCallback = (result: string) => void;
type ErrorCallback = (error: Error) => void;

function processWithCallbacks(
data: string[],
onProgress: ProgressCallback,
onComplete: CompleteCallback,
onError: ErrorCallback
): void {
try {
const total = data.length;
const results: string[] = [];

data.forEach((item, index) => {
// 各アイテムを処理
results.push(item.toUpperCase());
// 進捗を報告
onProgress(Math.round(((index + 1) / total) * 100));
});

// 完了を報告
onComplete(results.join(', '));
} catch (error) {
onError(error as Error);
}
}

// 使用例
processWithCallbacks(
['apple', 'banana', 'cherry'],
(progress) => console.log(`Progress: ${progress}%`),
(result) => console.log(`Complete: ${result}`),
(error) => console.error(`Error: ${error.message}`)
);
// Progress: 33%
// Progress: 67%
// Progress: 100%
// Complete: APPLE, BANANA, CHERRY

レストパラメータ

レストパラメータ(Rest Parameters)は、可変長の引数を配列として受け取る機能です。

基本的な使い方

// レストパラメータの基本構文: ...パラメータ名: 型[]
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
console.log(sum(5)); // 5
console.log(sum()); // 0

コード解説:

  • ...numbers: number[]: 任意の数のnumber型引数を配列として受け取る
  • 内部では通常の配列として操作可能

通常のパラメータとの組み合わせ

// 最初のパラメータは通常、残りはレストパラメータ
function multiply(multiplier: number, ...numbers: number[]): number[] {
return numbers.map((n) => n * multiplier);
}

console.log(multiply(2, 1, 2, 3, 4)); // [2, 4, 6, 8]
console.log(multiply(10, 5, 10, 15)); // [50, 100, 150]

// 複数の通常パラメータとレストパラメータ
function createMessage(
prefix: string,
suffix: string,
...words: string[]
): string {
return `${prefix} ${words.join(' ')} ${suffix}`;
}

console.log(createMessage('[', ']', 'Hello', 'World'));
// '[ Hello World ]'

レストパラメータの注意点

// レストパラメータは最後のパラメータでなければならない
// 正しい
function correct(multiplier: number, ...numbers: number[]) {
return numbers.map((n) => n * multiplier);
}

// 間違い: レストパラメータの後に他のパラメータがある
// function wrong(...numbers: number[], multiplier: number) {} // Error

// レストパラメータは1つだけ
// 間違い: 複数のレストパラメータは使えない
// function wrong(...a: number[], ...b: string[]) {} // Error

実践的な使用例

// 例1: ログ出力関数
function log(level: string, ...messages: string[]): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}]`, ...messages);
}

log('INFO', 'Application started');
log('ERROR', 'Something went wrong:', 'Connection failed');

// 例2: 複数の値の最大値を求める
function max(...numbers: number[]): number {
if (numbers.length === 0) {
return -Infinity;
}
return Math.max(...numbers);
}

console.log(max(5, 10, 3, 8, 15)); // 15
console.log(max(42)); // 42

// 例3: 文字列の結合
function joinStrings(separator: string, ...strings: string[]): string {
return strings.join(separator);
}

console.log(joinStrings(', ', 'Apple', 'Banana', 'Orange'));
// 'Apple, Banana, Orange'

console.log(joinStrings(' - ', 'Tokyo', 'Osaka', 'Kyoto'));
// 'Tokyo - Osaka - Kyoto'

// 例4: 配列をフラット化
function flatten<T>(...arrays: T[][]): T[] {
return arrays.reduce((acc, arr) => [...acc, ...arr], []);
}

console.log(flatten([1, 2], [3, 4], [5, 6])); // [1, 2, 3, 4, 5, 6]
console.log(flatten(['a'], ['b', 'c'])); // ['a', 'b', 'c']

thisの型付け

TypeScriptでは、関数内のthisの型を明示的に指定できます。

アロー関数とthis

アロー関数ではthisがレキシカルに束縛されます(定義時のthisを保持)。

class Counter {
count: number = 0;

// 従来の関数: thisが呼び出し元によって変わる可能性がある
incrementWrong() {
setTimeout(function() {
// this.count++; // Error: thisがCounterを指さない可能性
console.log('Wrong');
}, 1000);
}

// アロー関数: thisが定義時のコンテキスト(Counter)を保持
incrementCorrect() {
setTimeout(() => {
this.count++; // OK: thisがCounterインスタンスを指す
console.log(this.count);
}, 1000);
}
}

const counter = new Counter();
counter.incrementCorrect(); // 1秒後に1が表示される

thisパラメータ

関数の最初のパラメータとしてthisを指定することで、thisの型を明示できます。

interface User {
name: string;
greet(this: User): void;
}

const user: User = {
name: 'Alice',
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};

user.greet(); // "Hello, I'm Alice"

// 誤った呼び出し方をするとエラー
const greetFunc = user.greet;
// greetFunc(); // Error: The 'this' context... is not assignable

クラスでのthis

class EventHandler {
message: string = 'Button clicked';

setupEventListener() {
const button = document.getElementById('myButton');

// アロー関数を使う(thisが正しく動作)
button?.addEventListener('click', () => {
console.log(this.message); // 'Button clicked'
});
}
}

class NumberProcessor {
multiplier: number = 2;

processNumbers(numbers: number[]): number[] {
// アロー関数でthis.multiplierにアクセス
return numbers.map((n) => n * this.multiplier);
}
}

const processor = new NumberProcessor();
console.log(processor.processNumbers([1, 2, 3])); // [2, 4, 6]

試してみよう: 汎用的なフィルター関数を作ろう ★★★

以下の要件を満たす汎用的なフィルター関数群を作成してください。

要件:

  1. filterNumbers(numbers, ...predicates): 複数の条件関数を受け取り、すべての条件を満たす数値だけを返す
  2. filterByRange(min, max): 指定した範囲内の数値かどうかを判定する関数を返す
  3. filterByDivisible(divisor): 指定した数で割り切れるかどうかを判定する関数を返す
  4. createFilter(predicate): 条件関数を受け取り、配列をフィルタリングする関数を返す
ヒント
  1. filterNumbersではレストパラメータで複数の条件関数を受け取る
  2. filterByRangefilterByDivisibleは高階関数(関数を返す関数)として実装
  3. createFilterはジェネリクスを使って任意の型に対応させる
  4. 条件関数の型は(value: number) => boolean
回答と解説
// 条件関数の型を定義
type Predicate<T> = (value: T) => boolean;

// 複数の条件関数を受け取り、すべての条件を満たす数値だけを返す
function filterNumbers(
numbers: number[],
...predicates: Predicate<number>[]
): number[] {
return numbers.filter((num) =>
// すべての条件関数がtrueを返す場合のみtrueを返す
predicates.every((predicate) => predicate(num))
);
}

// 指定した範囲内の数値かどうかを判定する関数を返す
function filterByRange(min: number, max: number): Predicate<number> {
return (value: number) => value >= min && value <= max;
}

// 指定した数で割り切れるかどうかを判定する関数を返す
function filterByDivisible(divisor: number): Predicate<number> {
return (value: number) => value % divisor === 0;
}

// 条件関数を受け取り、配列をフィルタリングする関数を返す
function createFilter<T>(predicate: Predicate<T>): (items: T[]) => T[] {
return (items: T[]) => items.filter(predicate);
}

// テスト
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 18, 20];

// filterNumbersのテスト: 5以上15以下で、3で割り切れる数
const result1 = filterNumbers(
numbers,
filterByRange(5, 15),
filterByDivisible(3)
);
console.log(result1); // [6, 9, 12, 15]

// filterByRangeのテスト
const inRange = filterByRange(5, 10);
console.log(inRange(7)); // true
console.log(inRange(15)); // false

// filterByDivisibleのテスト
const isEven = filterByDivisible(2);
console.log(isEven(4)); // true
console.log(isEven(5)); // false

// createFilterのテスト
const filterPositive = createFilter<number>((n) => n > 0);
console.log(filterPositive([-2, -1, 0, 1, 2])); // [1, 2]

const filterLongStrings = createFilter<string>((s) => s.length > 3);
console.log(filterLongStrings(['a', 'ab', 'abc', 'abcd', 'abcde'])); // ['abcd', 'abcde']

解説:

  • Predicate<T>: 汎用的な条件関数の型。値を受け取ってbooleanを返す
  • filterNumbers: レストパラメータで複数の条件関数を受け取り、everyですべての条件をチェック
  • filterByRange: クロージャを使ってminmaxを保持する条件関数を返す
  • filterByDivisible: 同様にクロージャでdivisorを保持
  • createFilter: ジェネリクスを使って任意の型の配列に対応

高階関数のメリット:

  • 関数を組み合わせて複雑なフィルタリングを実現
  • 条件をパラメータ化して再利用可能に
  • コードの意図が明確になる

まとめ

この章で学んだこと:

  • 関数のオーバーロード: 同じ関数名で異なる型のパラメータ/戻り値を扱う
  • コールバック関数: 関数を引数として渡し、処理の流れを制御する
  • レストパラメータ: ...を使って可変長の引数を配列として受け取る
  • thisの型付け: アロー関数やthisパラメータでthisの型を明示する

次の章では、クラスの基本について学びます。TypeScriptのオブジェクト指向プログラミングの基礎を扱います。

初学者がつまずきやすいポイント

警告

よくある間違い

❌ オーバーロードの順序を間違える

// ❌ 間違い: より具体的なシグネチャを後に書いている
function process(value: string | number): string;
function process(value: string): string; // これは呼ばれない

// ✅ 正しい: より具体的なシグネチャを先に書く
function process(value: string): string;
function process(value: string | number): string;

function process(value: string | number): string {
return String(value);
}

原因: TypeScriptは上から順にシグネチャをチェックし、最初にマッチしたものを使用します。

解決策: 具体的なシグネチャを先に、汎用的なシグネチャを後に配置してください。

❌ コールバック関数の型が不正確

// ❌ 間違い: コールバックの引数の型を省略
function processItems(items: string[], callback) {
items.forEach(callback);
}

// ✅ 正しい: コールバックの型を明示
function processItems(
items: string[],
callback: (item: string, index: number) => void
): void {
items.forEach(callback);
}

原因: 引数の型が省略されると暗黙的にanyになります。

解決策: コールバック関数の引数と戻り値の型を明示的に定義してください。

❌ レストパラメータを最後以外に配置

// ❌ 間違い: レストパラメータが途中にある
function wrong(...numbers: number[], multiplier: number) {
// Error: A rest parameter must be last in a parameter list
}

// ✅ 正しい: レストパラメータは最後に配置
function correct(multiplier: number, ...numbers: number[]) {
return numbers.map(n => n * multiplier);
}

原因: JavaScriptの仕様上、レストパラメータは引数リストの最後である必要があります。

解決策: レストパラメータは必ず関数引数の最後に配置してください。

❌ thisのコンテキストを見失う

class Timer {
seconds: number = 0;

// ❌ 間違い: 通常の関数でthisを使用
startWrong() {
setInterval(function() {
this.seconds++; // Error: thisがTimerを指さない
}, 1000);
}

// ✅ 正しい: アロー関数でthisを保持
startCorrect() {
setInterval(() => {
this.seconds++; // OK: アロー関数はthisを保持
}, 1000);
}
}

原因: 通常の関数は呼び出し元によってthisが変わりますが、アロー関数は定義時のthisを保持します。

解決策: コールバック内でthisを使う場合はアロー関数を使用してください。

❌ 実装シグネチャを直接呼び出せると思う

function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string { // 実装シグネチャ
return String(value);
}

// ✅ OK: オーバーロードシグネチャに一致
format('hello'); // OK
format(123); // OK

// ❌ 間違い: 実装シグネチャは直接呼び出せない
format({ toString: () => 'test' }); // Error: オーバーロードにマッチしない

原因: 実装シグネチャは型チェックには使われず、オーバーロードシグネチャのみが外部から見える型となります。

解決策: 呼び出し可能な型はオーバーロードシグネチャで定義してください。


次に読む

クラスと OOP へ進みます。クラス、継承、アクセス修飾子、抽象クラスを使ったオブジェクト指向設計を学びます。