TypeScriptで関数を書くとき、引数の型や戻り値の型を正しく定義することは、型安全なコードを書く第一歩です。しかし、実務ではそれだけでは足りません。オプション引数、レストパラメータ、関数オーバーロード、ジェネリクス関数、型ガード関数など、関数の型定義には奥深いテクニックが数多く存在します。
この記事では、TypeScriptの関数に関する型定義を基本から実務レベルまで体系的に解説します。「引数の型はわかるけど、コールシグネチャって何?」「オーバーロードとジェネリクスのどちらを使うべき?」「非同期関数の戻り値型はどう書く?」といった疑問をすべて解消できる内容です。
この記事で学べること
- 引数・戻り値の型定義の基本と型推論の活用方法
- オプション引数・デフォルト値・レストパラメータの型定義
- 関数型(Function Type Expression)とコールシグネチャの使い分け
- アロー関数と function 宣言における型の違い
- void / never / undefined の正しい使い分け
- 関数オーバーロードで複数のシグネチャを定義する方法
- ジェネリクス関数で汎用的かつ型安全な関数を作る方法
- 型ガード関数(is キーワード)で型を絞り込むテクニック
- this パラメータ・コールバック・非同期関数・高階関数の型定義
- 実務で使える関数型パターン集(イベントハンドラー、API呼び出し、バリデーション等)
- よくあるエラーと対処法
前提知識:この記事はTypeScriptの基本型(string, number, boolean, 配列, オブジェクト型, Union型など)を理解している方を対象としています。基本型から学びたい方は【TypeScript】型の書き方 完全入門を先にお読みください。
関数の型定義の基本(引数の型・戻り値の型)
TypeScriptで関数を定義する際、最も基本的な型注釈は引数の型と戻り値の型の2つです。まずはこの基本をしっかり押さえましょう。
引数に型を付ける
関数の引数には、パラメータ名の後に: 型名を付けて型注釈を記述します。
引数の型注釈
// 引数に型を付ける
function greet(name: string) {
console.log(`Hello, ${name}!`);
}
// 複数の引数
function add(a: number, b: number) {
return a + b;
}
greet('TypeScript'); // OK
greet(42); // エラー: number は string に割り当てられない
add(1, 2); // OK: 3
add('1', 2); // エラー: string は number に割り当てられない
引数に型注釈がない場合、TypeScriptは暗黙的に any 型と推論します。tsconfig.json で "strict": true または "noImplicitAny": true が有効な場合、型注釈のない引数はコンパイルエラーになります。
戻り値に型を付ける
関数の戻り値の型は、引数リストの閉じ括弧 ) の後に : 型名 で記述します。
戻り値の型注釈
// 戻り値の型を明示的に指定
function add(a: number, b: number): number {
return a + b;
}
// 文字列を返す関数
function getFullName(first: string, last: string): string {
return `${first} ${last}`;
}
// boolean を返す関数
function isAdult(age: number): boolean {
return age >= 18;
}
// 戻り値の型が合わないとエラー
function multiply(a: number, b: number): number {
return `${a} * ${b}`; // エラー: string を number に割り当てられない
}
戻り値の型推論
TypeScriptは return 文の値から戻り値の型を自動推論します。そのため、戻り値の型注釈は省略可能です。
戻り値の型推論
// 戻り値の型を省略 → TypeScript が number と推論
function add(a: number, b: number) {
return a + b; // 推論: number
}
// 複数の return がある場合は Union 型に推論
function parse(input: string) {
const num = Number(input);
if (isNaN(num)) {
return null; // null
}
return num; // number
}
// 推論結果: function parse(input: string): number | null
戻り値の型注釈を書くべきケース
- 公開APIや関数ライブラリ:利用者に戻り値の型を明示するため
- 再帰関数:TypeScriptが推論できないことがあるため
- 複雑なロジック:意図しない型の return を防ぐガードとして
- チーム開発:コードレビューで戻り値が一目でわかるようにするため
逆に、簡単な内部関数では型推論に任せても問題ありません。
オブジェクト型の引数
関数の引数にオブジェクトを渡す場合、オブジェクトの各プロパティの型を定義します。
オブジェクト型の引数
// インラインでオブジェクト型を定義
function printUser(user: { name: string; age: number }) {
console.log(`${user.name} (${user.age}歳)`);
}
// type エイリアスを使う(推奨)
type User = {
name: string;
age: number;
email: string;
};
function createUser(user: User): User {
return { ...user };
}
// interface を使う
interface Product {
id: number;
name: string;
price: number;
}
function formatPrice(product: Product): string {
return `${product.name}: ${product.price}円`;
}
ポイント:プロパティが3つ以上になる場合や、複数の関数で同じ型を使い回す場合は type エイリアスや interface で名前を付けましょう。コードの可読性が大幅に向上します。
分割代入と型注釈
引数をオブジェクトの分割代入で受け取る場合の型注釈の書き方です。
分割代入引数の型注釈
// 分割代入 + 型注釈
function greet({ name, age }: { name: string; age: number }) {
console.log(`${name}さん(${age}歳)`);
}
// type エイリアスを使って読みやすく
type Config = {
host: string;
port: number;
secure: boolean;
};
function connect({ host, port, secure }: Config) {
const protocol = secure ? 'https' : 'http';
return `${protocol}://${host}:${port}`;
}
// 注意: 分割代入のデフォルト値と型注釈を混同しない
// NG: function fn({ name: string }) → これは name を string にリネームする構文!
// OK: function fn({ name }: { name: string })
注意:{ name: string } と書くと、TypeScriptではなくJavaScriptの分割代入のリネーム構文(name を string という変数名に変更)として解釈されます。型注釈は { name }: { name: string } のように分割代入パターンの外に書きましょう。
オプション引数とデフォルト値
関数の引数は、すべてが必須とは限りません。TypeScriptではオプション引数(省略可能な引数)とデフォルト値付き引数を使って柔軟な関数シグネチャを定義できます。
オプション引数(? 修飾子)
パラメータ名の後ろに ? を付けると、その引数は省略可能になります。省略された場合、値は undefined になります。
オプション引数の基本
// greeting はオプション引数
function greet(name: string, greeting?: string) {
const msg = greeting ?? 'Hello';
return `${msg}, ${name}!`;
}
greet('Alice'); // "Hello, Alice!"
greet('Alice', 'Hi'); // "Hi, Alice!"
greet('Alice', undefined); // "Hello, Alice!"(明示的にundefined)
// オプション引数の型は string | undefined になる
// そのため、使う前にチェックが必要
function printLength(text?: string) {
// text.length; // エラー: text は undefined の可能性がある
console.log(text?.length); // OK: オプショナルチェーン
if (text !== undefined) {
console.log(text.length); // OK: 型が string に絞り込まれる
}
}
注意:オプション引数は必須引数より後ろに置く必要があります。function fn(a?: string, b: number) はコンパイルエラーです。必須引数を先、オプション引数を後に記述しましょう。
デフォルト値付き引数
引数にデフォルト値を設定すると、その引数は自動的にオプショナルになります。? を付ける必要はありません。
デフォルト値付き引数
// デフォルト値を指定すると自動的にオプショナルになる
function greet(name: string, greeting: string = 'Hello') {
return `${greeting}, ${name}!`;
}
greet('Alice'); // "Hello, Alice!"
greet('Alice', 'Hi'); // "Hi, Alice!"
// デフォルト値から型が推論されるので型注釈は省略可能
function createConfig(
host = 'localhost', // string と推論
port = 3000, // number と推論
secure = false // boolean と推論
) {
return { host, port, secure };
}
// デフォルト値 vs オプション引数の違い
// デフォルト値: undefined が渡された場合もデフォルト値が使われる
greet('Alice', undefined); // "Hello, Alice!" (デフォルト値が適用)
| 比較項目 |
オプション引数(?) |
デフォルト値 |
| 構文 |
param?: Type |
param: Type = value |
| 省略時の値 |
undefined |
デフォルト値 |
| undefined を渡した場合 |
undefined のまま |
デフォルト値が使われる |
| 型注釈 |
必要 |
推論可能(省略OK) |
| null チェック |
必要 |
不要(デフォルト値あり) |
実務でのおすすめ:単に省略可能にしたいだけなら ?、省略時に特定の値を使いたいならデフォルト値を使いましょう。デフォルト値を使うと undefined チェックが不要になるため、コードがシンプルになります。
レストパラメータ(…args)の型定義
レストパラメータ(rest parameter)を使うと、任意の数の引数を配列として受け取ることができます。型定義では配列型を指定します。
基本的なレストパラメータ
レストパラメータの基本
// ...numbers は number[] 型になる
function sum(...numbers: number[]) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20); // 30
sum(); // 0(空配列)
// 固定引数 + レストパラメータ
function log(level: string, ...messages: string[]) {
console.log(`[${level}]`, ...messages);
}
log('INFO', 'Server started', 'port: 3000');
// [INFO] Server started port: 3000
タプル型のレストパラメータ
TypeScript 4.0以降では、レストパラメータにタプル型を使うことで、引数の数と各位置の型を厳密に制御できます。
タプル型のレストパラメータ
// タプル型で引数の位置ごとの型を指定
function createUser(...args: [string, number, string]) {
const [name, age, email] = args;
return { name, age, email };
}
createUser('Alice', 30, 'alice@example.com'); // OK
createUser('Alice', 30); // エラー: 3つの引数が必要
// オプショナルなタプル要素
function point(...coords: [number, number, number?]) {
const [x, y, z] = coords;
return z !== undefined ? { x, y, z } : { x, y };
}
point(1, 2); // OK: 2D座標
point(1, 2, 3); // OK: 3D座標
// レストパラメータ内のレスト(可変長タプル)
function format(...args: [string, ...number[]]) {
const [label, ...values] = args;
return `${label}: ${values.join(', ')}`;
}
format('scores', 90, 85, 70); // "scores: 90, 85, 70"
format('empty'); // "empty: "
ポイント:配列リテラルを関数にスプレッドする場合、as const をつけると readonly タプル型になり、型チェックがより厳密になります。特にタプル型のレストパラメータを受け取る関数に渡す場合は as const が必須です。
関数型(Function Type Expression)
TypeScriptでは、関数自体の型を関数型式(Function Type Expression)で表現できます。変数やコールバックの型を定義する際に使います。
関数型の基本構文
関数型は (引数: 型, ...) => 戻り値の型 という構文で記述します。
関数型の基本
// 関数型を変数に注釈する
let greet: (name: string) => string;
greet = (name) => `Hello, ${name}!`; // OK
greet = (name) => 42; // エラー: number は string に割り当てられない
// type エイリアスで関数型を定義
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;
const divide: MathOperation = (a, b) => a / b;
// 関数型を引数として受け取る
function calculate(a: number, b: number, operation: MathOperation): number {
return operation(a, b);
}
calculate(10, 5, add); // 15
calculate(10, 5, multiply); // 50
calculate(10, 5, divide); // 2
注意:関数型の => とアロー関数の => は別物です。関数型の => は「戻り値の型」を示し、アロー関数の => は「関数本体」を示します。
関数型のパラメータ名は省略できない
関数型を記述する際、パラメータ名は省略できません。ただし、パラメータ名はドキュメント目的のみで、実際の変数名とは無関係です。
パラメータ名は必須
// NG: パラメータ名がない
// type Fn = (string, number) => boolean;
// OK: パラメータ名が必要
type Fn = (input: string, index: number) => boolean;
// パラメータ名は実際の引数名と一致する必要はない
const fn: Fn = (s, i) => s.length > i; // OK
コールシグネチャ(Call Signature)
関数型式よりも多くの情報を持たせたい場合は、コールシグネチャを使ってオブジェクト型の中に関数の呼び出し方を定義します。関数にプロパティを持たせたい場合に特に有用です。
コールシグネチャ
// 関数型式
type FnExpression = (a: number) => string;
// コールシグネチャ(オブジェクト型の中に書く)
type FnCallSignature = {
(a: number): string; // => ではなく : を使う
};
// コールシグネチャの利点: プロパティを追加できる
type Logger = {
(message: string): void; // 関数として呼べる
level: 'info' | 'warn' | 'error'; // プロパティも持てる
history: string[]; // 配列プロパティ
};
// Logger 型を満たすオブジェクトを作る
const logger: Logger = Object.assign(
(message: string) => {
console.log(`[${logger.level}] ${message}`);
logger.history.push(message);
},
{ level: 'info' as const, history: [] as string[] }
);
logger('Hello'); // [info] Hello
logger.level = 'warn';
logger('Something wrong'); // [warn] Something wrong
interface でコールシグネチャを定義する
interface のコールシグネチャ
// interface でも同じことができる
interface Formatter {
(value: string): string; // コールシグネチャ
locale: string; // プロパティ
reset(): void; // メソッド
}
// コンストラクトシグネチャ(new で呼べる型)
interface DateConstructor {
new (): Date; // new DateConstructor()
new (value: number): Date; // new DateConstructor(1234567890)
new (value: string): Date; // new DateConstructor("2025-01-01")
}
| 比較項目 |
関数型式 |
コールシグネチャ |
| 構文 |
(a: T) => R |
{ (a: T): R } |
| プロパティの追加 |
不可 |
可能 |
| オーバーロード |
不可 |
可能(複数シグネチャ) |
| コンストラクタ |
不可 |
可能(new シグネチャ) |
| 使い分け |
シンプルな関数型 |
プロパティ付き関数やオーバーロード |
アロー関数 vs function 宣言の型の違い
TypeScriptではアロー関数と function 宣言の両方で型注釈を付けることができますが、書き方や挙動にいくつかの違いがあります。
型注釈の書き方の違い
function 宣言 vs アロー関数
// ====== function 宣言 ======
function add(a: number, b: number): number {
return a + b;
}
// ====== アロー関数(パラメータに注釈)======
const add2 = (a: number, b: number): number => a + b;
// ====== アロー関数(変数に関数型を注釈)======
const add3: (a: number, b: number) => number = (a, b) => a + b;
// ====== type エイリアスで分離(推奨)======
type AddFn = (a: number, b: number) => number;
const add4: AddFn = (a, b) => a + b; // 引数の型注釈は不要
巻き上げ(Hoisting)の違い
function 宣言は巻き上げ(hoisting)されるため、宣言より前に呼び出せます。一方、アロー関数は変数に代入するため、巻き上げされません。
巻き上げの違い
// function 宣言は巻き上げされる
sayHello('Alice'); // OK: "Hello, Alice!"
function sayHello(name: string): string {
return `Hello, ${name}!`;
}
// アロー関数は巻き上げされない
// sayHi('Bob'); // エラー: 初期化前に使用できません
const sayHi = (name: string): string => {
return `Hi, ${name}!`;
};
this の扱いの違い
アロー関数は自身の this を持たず、外側のスコープの this をキャプチャします。この違いは後述する「thisパラメータの型定義」で詳しく解説します。
this の扱い
const counter = {
count: 0,
// function: this は呼び出し元に依存
increment() {
this.count++;
},
// アロー関数: this は外側スコープから取得
delayedIncrement() {
setTimeout(() => {
this.count++; // OK: this は counter を参照
}, 1000);
},
// function で同じことをすると this が失われる
buggyDelayedIncrement() {
setTimeout(function() {
// this.count++; // 実行時エラー: this は undefined
}, 1000);
}
};
| 比較項目 |
function 宣言 |
アロー関数 |
| 巻き上げ |
あり |
なし |
| this のバインド |
呼び出し時に決まる |
定義時のスコープを継承 |
| arguments オブジェクト |
あり |
なし |
| ジェネレータ(function*) |
可能 |
不可 |
| オーバーロード |
可能 |
不可(型注釈で対応) |
| コンテキスト型付け |
引数の型注釈必須 |
変数の型注釈から推論可能 |
void / never / undefined の使い分け
関数の戻り値型として使われる void、never、undefined は、それぞれ意味が異なります。正しく使い分けることで、コードの意図を明確に表現できます。
void:戻り値を使わないことの宣言
void は「戻り値を使わない・使うべきでない」ことを表します。関数が何も返さない(または return; だけの場合)に使います。
void 型
// 戻り値を返さない関数
function printMessage(msg: string): void {
console.log(msg);
// return 文なし、または return; だけ
}
// void は「値がないこと」ではなく「戻り値を無視すること」を意味する
const result = printMessage('hello'); // result の型は void
// void 型のコールバック: 戻り値があっても無視される
type Callback = () => void;
const cb: Callback = () => 42; // OK! void コールバックは戻り値を持てる
const val = cb(); // val の型は void(42 ではない)
// 実用例: forEach のコールバックは void
[1, 2, 3].forEach((n) => n * 2); // OK: 戻り値は無視される
void のコールバックに関する重要な挙動
コールバック型が () => void の場合、実装が値を返しても型エラーにはなりません。これはTypeScriptの仕様で、Array.prototype.forEach のコールバックに Array.prototype.push(数値を返す)を渡せるようにするためです。ただし、関数宣言で直接 void を指定した場合は、明示的に値を返すとエラーになります。
never:関数が正常に終了しないことの宣言
never は「関数が決して値を返さない」ことを表します。例外をスローする関数や無限ループの関数に使います。
never 型
// 例外をスローする関数
function throwError(message: string): never {
throw new Error(message);
}
// 無限ループ
function infiniteLoop(): never {
while (true) {
// 永遠に終わらない
}
}
// never の実用的な使い方: 網羅性チェック(Exhaustive Check)
type Shape = 'circle' | 'square' | 'triangle';
function getArea(shape: Shape): number {
switch (shape) {
case 'circle':
return Math.PI * 10 ** 2;
case 'square':
return 10 * 10;
case 'triangle':
return (10 * 5) / 2;
default: {
// shape は never 型 → 全ケースを網羅しているか確認
const _exhaustive: never = shape;
return _exhaustive;
}
}
}
undefined:undefined を返すことの明示
undefined を戻り値の型にすると、関数が明示的に undefined を返す必要があります。void との違いに注意しましょう。
undefined vs void
// void: return 文は不要
function logVoid(): void {
console.log('void');
// return 文なしでOK
}
// undefined: 明示的に return undefined が必要
function logUndefined(): undefined {
console.log('undefined');
return undefined; // または return;
}
// 実用例: Map.get() のような「見つからない場合」の表現
function findUser(id: number): User | undefined {
const users = [{ id: 1, name: 'Alice' }];
return users.find(u => u.id === id); // User | undefined
}
| 型 |
意味 |
return 文 |
使いどころ |
void |
戻り値を使わない |
不要 |
副作用のみの関数、コールバック |
never |
関数が正常に終了しない |
到達不能 |
例外スロー、無限ループ、網羅性チェック |
undefined |
undefined を返す |
必要 |
値が存在しない場合の表現 |
関数オーバーロード(Function Overloads)
関数オーバーロードとは、同じ関数名で複数のシグネチャ(呼び出し方のパターン)を定義する機能です。引数の型や数に応じて異なる戻り値の型を返したい場合に使います。
オーバーロードの基本構文
オーバーロードは「オーバーロードシグネチャ」と「実装シグネチャ」の2つで構成されます。
オーバーロードの基本
// オーバーロードシグネチャ(呼び出し側から見えるシグネチャ)
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
// 実装シグネチャ(呼び出し側からは見えない)
function format(value: string | number | Date): string {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value.toFixed(2);
} else {
return value.toISOString();
}
}
format('hello'); // "HELLO" (型: string)
format(3.14159); // "3.14" (型: string)
format(new Date()); // "2025-..." (型: string)
format(true); // エラー: boolean に一致するオーバーロードなし
注意:実装シグネチャは呼び出し側から直接使うことはできません。呼び出し側が使えるのはオーバーロードシグネチャだけです。実装シグネチャの型は、すべてのオーバーロードシグネチャと互換性がなければなりません。
引数の数が異なるオーバーロード
引数の数が異なるオーバーロード
// 引数1つ: 要素を作成
function createElement(tag: string): HTMLElement;
// 引数2つ: 要素を作成してテキスト設定
function createElement(tag: string, text: string): HTMLElement;
// 引数3つ: 要素を作成してテキストとクラス設定
function createElement(tag: string, text: string, className: string): HTMLElement;
// 実装
function createElement(tag: string, text?: string, className?: string): HTMLElement {
const el = document.createElement(tag);
if (text) el.textContent = text;
if (className) el.className = className;
return el;
}
戻り値の型が異なるオーバーロード
オーバーロードの最も重要なユースケースは、引数の型に応じて戻り値の型が変わる場合です。
戻り値が異なるオーバーロード
// string を渡したら string を返す
function parse(input: string): string;
// number を渡したら number[] を返す
function parse(input: number): number[];
function parse(input: string | number): string | number[] {
if (typeof input === 'string') {
return input.trim();
}
return Array.from({ length: input }, (_, i) => i);
}
// 呼び出し側では戻り値の型が正確に推論される
const str = parse(' hello '); // string 型
const arr = parse(5); // number[] 型
オーバーロード vs Union型 vs ジェネリクス
オーバーロードを使うべきかどうかの判断基準を整理します。
3つのアプローチの比較
// ========== Union型 ==========
// 戻り値の型が共通の場合はこれで十分
function toStringUnion(value: string | number): string {
return String(value);
}
// ========== オーバーロード ==========
// 入力と出力の型に対応関係がある場合
function processOverload(input: string): string;
function processOverload(input: number): number;
function processOverload(input: string | number) {
return input;
}
// ========== ジェネリクス ==========
// 入力と出力が同じ型の場合
function identity<T>(value: T): T {
return value;
}
| アプローチ |
適したケース |
注意点 |
| Union型 |
戻り値の型が引数に依存しない場合 |
最もシンプル、まずこれを検討 |
| オーバーロード |
引数と戻り値に明確な対応関係がある場合 |
実装シグネチャの型が広くなりがち |
| ジェネリクス |
型パラメータで関係を表現できる場合 |
最も柔軟、型が複雑になることも |
実務でのガイドライン
- まずUnion型で書けないかを検討する(TypeScript公式ドキュメントでも推奨)
- Union型では表現できない引数と戻り値の対応関係がある場合にオーバーロード
- 入力の型をそのまま出力に使う場合はジェネリクスがベスト
- オーバーロードシグネチャは具体的な型から順に書く(より具体的なものが先にマッチする)
ジェネリクス関数(Generic Functions)
ジェネリクス関数は、呼び出し時に具体的な型が決まる型パラメータを使って、汎用的かつ型安全な関数を定義する仕組みです。型を引数のように扱うことで、異なる型に対して同じロジックを再利用できます。
ジェネリクスの基本
ジェネリクス関数の基本
// T は型パラメータ(型変数)
function identity<T>(value: T): T {
return value;
}
// 型引数を明示的に指定
const str = identity<string>('hello'); // string 型
const num = identity<number>(42); // number 型
// 型推論に任せる(推奨)
const str2 = identity('hello'); // string 型(推論)
const num2 = identity(42); // number 型(推論)
// 複数の型パラメータ
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair('name', 42); // [string, number] 型
型パラメータの制約(extends)
型パラメータに extends を使って制約を付けることで、特定の構造を持つ型だけに限定できます。
型パラメータの制約
// T は length プロパティを持つ型に制約
function getLength<T extends { length: number }>(value: T): number {
return value.length; // T が length を持つことが保証される
}
getLength('hello'); // 5 (string は length を持つ)
getLength([1, 2, 3]); // 3 (配列は length を持つ)
getLength(42); // エラー: number に length プロパティはない
// インターフェースで制約を定義
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const user = findById(users, 1); // { id: number; name: string } | undefined
keyof を使ったジェネリクス
keyof と組み合わせると、オブジェクトのプロパティ名を型安全に扱えます。
keyof とジェネリクス
// オブジェクトから型安全にプロパティを取得
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30, email: 'alice@example.com' };
const name = getProperty(user, 'name'); // string 型
const age = getProperty(user, 'age'); // number 型
// getProperty(user, 'address'); // エラー: 'address' は keyof User にない
// 複数のキーを受け取ってオブジェクトのサブセットを返す
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
const partial = pick(user, 'name', 'email');
// { name: string; email: string } 型
ジェネリクスのデフォルト型
デフォルト型パラメータ
// デフォルト型を指定
function createArray<T = string>(length: number, value: T): T[] {
return Array.from({ length }, () => value);
}
const strs = createArray(3, 'hello'); // string[](推論)
const nums = createArray(3, 0); // number[](推論)
// 制約 + デフォルト型
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
function fetchData<T = unknown>(url: string): Promise<ApiResponse<T>> {
return fetch(url).then(r => r.json());
}
// 型引数を指定して使う
const res = await fetchData<User[]>('/api/users');
res.data; // User[] 型
ポイント:ジェネリクスの型パラメータ名は慣例的に1文字の大文字(T, U, K, V など)を使います。T = Type、K = Key、V = Value、E = Element の略です。意味がわかりにくい場合は TItem, TResult のように T プレフィックスで命名することもあります。
型ガード関数(Type Predicates: is)
型ガード関数は、戻り値の型に value is Type という型述語(Type Predicate)を使うことで、呼び出し元の型を絞り込む関数です。typeof や instanceof では対応できない複雑な型判定に威力を発揮します。
型ガード関数の基本
型ガード関数の基本
interface Cat {
type: 'cat';
meow: () => void;
}
interface Dog {
type: 'dog';
bark: () => void;
}
type Animal = Cat | Dog;
// 型ガード関数: 戻り値の型が「animal is Cat」
function isCat(animal: Animal): animal is Cat {
return animal.type === 'cat';
}
function handleAnimal(animal: Animal) {
if (isCat(animal)) {
animal.meow(); // OK: animal は Cat 型に絞り込まれる
} else {
animal.bark(); // OK: animal は Dog 型に絞り込まれる
}
}
実践的な型ガード関数
実践的な型ガード関数
// null/undefined を除外する型ガード
function isNotNullish<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
// 配列のフィルタリングで活躍
const mixed = ['a', null, 'b', undefined, 'c'];
const strings = mixed.filter(isNotNullish); // string[] 型
// string であることを判定する型ガード
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// 特定のプロパティを持つことを判定する型ガード
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return typeof obj === 'object' && obj !== null && key in obj;
}
// API レスポンスのバリデーション
interface SuccessResponse {
status: 'success';
data: unknown;
}
interface ErrorResponse {
status: 'error';
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(res: ApiResponse): res is SuccessResponse {
return res.status === 'success';
}
注意:型ガード関数の実装が間違っていても、TypeScriptはコンパイル時にチェックしません。例えば return true とだけ書いても型エラーにはなりません。型ガード関数は「開発者が正しさを保証する」仕組みなので、ロジックのミスに注意しましょう。
this パラメータの型定義
TypeScriptでは関数の最初のパラメータとして this の型を宣言できます。これはJavaScriptの this の動的な挙動をコンパイル時にチェックするための仕組みです。
this パラメータの基本
this パラメータ
// this パラメータは関数の最初のパラメータとして書く
// JavaScript に出力されるときは削除される
interface Button {
label: string;
disabled: boolean;
click(this: Button): void;
}
const button: Button = {
label: 'Submit',
disabled: false,
click() {
// this の型は Button 型であることが保証される
console.log(`Clicked: ${this.label}`);
this.disabled = true;
}
};
button.click(); // OK
// this が正しくバインドされていないとエラー
const fn = button.click;
// fn(); // エラー: void 型の this には Button を割り当てられない
メソッドチェーンの型定義
this を戻り値の型として使うと、メソッドチェーンが型安全に実装できます。
this を使ったメソッドチェーン
class QueryBuilder {
private conditions: string[] = [];
private sortField?: string;
where(condition: string): this {
this.conditions.push(condition);
return this; // this を返すことでチェーン可能に
}
orderBy(field: string): this {
this.sortField = field;
return this;
}
build(): string {
let query = 'SELECT * FROM table';
if (this.conditions.length) {
query += ' WHERE ' + this.conditions.join(' AND ');
}
if (this.sortField) {
query += ' ORDER BY ' + this.sortField;
}
return query;
}
}
// メソッドチェーンで使う
const sql = new QueryBuilder()
.where('age > 20')
.where('status = active')
.orderBy('name')
.build();
コールバック関数の型定義パターン
コールバック関数は、別の関数に引数として渡す関数のことです。TypeScriptではコールバックの型を正しく定義することで、呼び出し側と実装側の両方で型安全性を確保できます。
基本的なコールバックの型定義
コールバックの型定義
// インラインで定義
function fetchData(
url: string,
onSuccess: (data: string) => void,
onError: (error: Error) => void
) {
// ...
}
// type エイリアスで分離(再利用性が高い)
type SuccessCallback<T> = (data: T) => void;
type ErrorCallback = (error: Error) => void;
function fetchUsers(
onSuccess: SuccessCallback<User[]>,
onError: ErrorCallback
) {
// ...
}
// Node.js スタイルのコールバック(エラーファースト)
type NodeCallback<T> = (error: Error | null, data?: T) => void;
function readFile(path: string, callback: NodeCallback<string>) {
// ...
}
readFile('/path/to/file', (err, data) => {
if (err) {
console.error(err.message);
return;
}
console.log(data); // string | undefined
});
イベントリスナーのコールバック型
イベントリスナーの型定義
// カスタムイベントシステム
type EventMap = {
click: { x: number; y: number };
change: { value: string };
submit: { formData: FormData };
};
type EventHandler<T> = (event: T) => void;
function on<K extends keyof EventMap>(
event: K,
handler: EventHandler<EventMap[K]>
) {
// ...
}
// 型安全: event 名に応じてコールバックの引数型が決まる
on('click', (e) => {
console.log(e.x, e.y); // OK: e は { x: number; y: number }
});
on('change', (e) => {
console.log(e.value); // OK: e は { value: string }
});
非同期関数(async/await)の型定義
async 関数は必ず Promise を返します。TypeScriptでは戻り値の型を Promise<T> で明示するか、型推論に任せることができます。
async関数の基本型
async関数の型定義
// async関数の戻り値は常に Promise<T>
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Promise 内で返す値は User 型
}
// アロー関数版
const fetchUsers = async (): Promise<User[]> => {
const response = await fetch('/api/users');
return response.json();
};
// void を返す async関数
async function saveUser(user: User): Promise<void> {
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
// 何も返さない → Promise<void>
}
エラーハンドリングの型定義
async関数のエラーハンドリング
// Result型パターン(エラーを戻り値で表現)
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data: T = await response.json();
return { success: true, data };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e : new Error(String(e))
};
}
}
// 使い方
const result = await safeFetch<User[]>('/api/users');
if (result.success) {
console.log(result.data); // User[] 型
} else {
console.error(result.error); // Error 型
}
Promise関連のユーティリティ型
Promise関連のユーティリティ
// Awaited<T>: Promise を再帰的にアンラップ
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number(ネストも解決)
// ReturnType + Awaited で async 関数の実質的な戻り値型を取得
async function getUser() {
return { id: 1, name: 'Alice' };
}
type UserType = Awaited<ReturnType<typeof getUser>>;
// { id: number; name: string }
// Promise.all の型(自動推論)
async function loadDashboard() {
const [users, posts, comments] = await Promise.all([
fetchUsers(), // Promise<User[]>
fetchPosts(), // Promise<Post[]>
fetchComments(), // Promise<Comment[]>
]);
// users: User[], posts: Post[], comments: Comment[]
return { users, posts, comments };
}
ポイント:async 関数では Promise<T> の T の部分を戻り値として書きます。TypeScriptが自動的に Promise で包んでくれるため、return 文で Promise を返す必要はありません(return data だけでOK)。
高階関数・カリー化の型定義
高階関数(Higher-Order Function)とは、関数を引数として受け取るか、関数を返す関数のことです。TypeScriptでは高階関数の型定義にジェネリクスを組み合わせることで、型安全性を保ったまま強力な抽象化が可能になります。
関数を返す関数
関数を返す関数
// 乗数を固定する関数を返す
function multiplier(factor: number): (value: number) => number {
return (value) => value * factor;
}
const double = multiplier(2); // (value: number) => number
const triple = multiplier(3); // (value: number) => number
double(5); // 10
triple(5); // 15
// ジェネリクスを使った高階関数
function createFilter<T>(
predicate: (item: T) => boolean
): (items: T[]) => T[] {
return (items) => items.filter(predicate);
}
const filterAdults = createFilter<User>(u => u.age >= 18);
const adults = filterAdults(users); // User[]
カリー化(Currying)の型定義
カリー化とは、複数の引数を受け取る関数を、1つずつ引数を受け取る関数のチェーンに変換することです。
カリー化
// 手動カリー化
function add(a: number): (b: number) => number {
return (b) => a + b;
}
const add5 = add(5); // (b: number) => number
add5(3); // 8
// ジェネリクスで汎用カリー化ユーティリティ(2引数版)
function curry2<A, B, R>(
fn: (a: A, b: B) => R
): (a: A) => (b: B) => R {
return (a) => (b) => fn(a, b);
}
const curriedAdd = curry2((a: number, b: number) => a + b);
curriedAdd(1)(2); // 3
// 実務例: API クライアント生成
function createApiClient(baseUrl: string) {
return function <T>(endpoint: string): Promise<T> {
return fetch(`${baseUrl}${endpoint}`).then(r => r.json());
};
}
const api = createApiClient('https://api.example.com');
const users = await api<User[]>('/users'); // User[]
パイプライン・コンポーズの型定義
パイプライン関数
// pipe: 関数を左から右に合成
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B;
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C;
function pipe<A, B, C, D>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): (a: A) => D;
function pipe(...fns: Function[]) {
return (input: unknown) =>
fns.reduce((acc, fn) => fn(acc), input);
}
// 使用例
const process = pipe(
(s: string) => s.trim(),
(s: string) => s.toUpperCase(),
(s: string) => s.split(' ')
);
process(' hello world '); // ["HELLO", "WORLD"]
高階関数の型定義で押さえるべきポイント
- 戻り値が関数の場合、戻り値の型を関数型で書く(
(...) => ReturnType)
- ジェネリクスと組み合わせることで、入力と出力の型関係を保持できる
- カリー化では 型パラメータを段階的に解決する設計がポイント
pipe のようなユーティリティはオーバーロードで複数の引数パターンをカバーする
実務で使える関数型パターン集
ここまで学んだ関数の型定義テクニックを組み合わせて、実務でよく使われるパターンを紹介します。
パターン1: イベントハンドラーの型定義
イベントハンドラー
// React のイベントハンドラー型
import { ChangeEvent, FormEvent, MouseEvent } from 'react';
// 入力フォームのハンドラー
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.value);
};
// フォーム送信ハンドラー
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
// フォームの処理
};
// ジェネリックなイベントハンドラー型
type Handler<E extends HTMLElement> = (
event: MouseEvent<E>
) => void;
const handleButtonClick: Handler<HTMLButtonElement> = (e) => {
console.log(e.currentTarget.textContent);
};
パターン2: API呼び出し関数の型定義
型安全なAPIクライアント
// APIのエンドポイント定義
interface ApiEndpoints {
'/users': {
GET: { response: User[] };
POST: { body: CreateUserDto; response: User };
};
'/users/:id': {
GET: { response: User };
PUT: { body: UpdateUserDto; response: User };
DELETE: { response: void };
};
}
// 型安全な fetch ラッパー
async function apiCall<
Path extends keyof ApiEndpoints,
Method extends keyof ApiEndpoints[Path]
>(
path: Path,
method: Method,
...args: 'body' extends keyof ApiEndpoints[Path][Method]
? [body: ApiEndpoints[Path][Method]['body']]
: []
): Promise<ApiEndpoints[Path][Method]['response']> {
// 実装...
}
// 使用例: 型安全なAPI呼び出し
const users = await apiCall('/users', 'GET'); // User[]
const newUser = await apiCall('/users', 'POST', {
name: 'Alice', email: 'alice@example.com'
}); // User
パターン3: バリデーション関数の型定義
バリデーション関数
// バリデーション結果型
type ValidationResult =
| { valid: true }
| { valid: false; errors: string[] };
// バリデーター型
type Validator<T> = (value: T) => ValidationResult;
// バリデーターを合成する関数
function compose<T>(
...validators: Validator<T>[]
): Validator<T> {
return (value) => {
const errors: string[] = [];
for (const validate of validators) {
const result = validate(value);
if (!result.valid) errors.push(...result.errors);
}
return errors.length === 0
? { valid: true }
: { valid: false, errors };
};
}
// 個別のバリデーター
const minLength = (min: number): Validator<string> =>
(value) => value.length >= min
? { valid: true }
: { valid: false, errors: [`${min}文字以上必要です`] };
const isEmail: Validator<string> = (value) =>
value.includes('@')
? { valid: true }
: { valid: false, errors: ['メールアドレスの形式が不正です'] };
// 合成して使う
const validateEmail = compose(minLength(5), isEmail);
validateEmail('ab'); // { valid: false, errors: ["5文字以上必要です", "メールアドレスの形式が不正です"] }
パターン4: ファクトリー関数の型定義
ファクトリー関数
// ファクトリーパターン
interface Shape {
kind: string;
area(): number;
}
interface Circle extends Shape {
kind: 'circle';
radius: number;
}
interface Rectangle extends Shape {
kind: 'rectangle';
width: number;
height: number;
}
// オーバーロードを使ったファクトリー関数
function createShape(kind: 'circle', radius: number): Circle;
function createShape(kind: 'rectangle', width: number, height: number): Rectangle;
function createShape(kind: string, ...args: number[]): Shape {
if (kind === 'circle') {
const radius = args[0];
return {
kind: 'circle', radius,
area: () => Math.PI * radius ** 2
};
}
const [width, height] = args;
return {
kind: 'rectangle', width, height,
area: () => width * height
};
}
const circle = createShape('circle', 10); // Circle 型
const rect = createShape('rectangle', 5, 10); // Rectangle 型
パターン5: ミドルウェアパターンの型定義
ミドルウェアパターン
// Express風のミドルウェア型
type Request = { url: string; headers: Record<string, string> };
type Response = { status: number; body: unknown };
type NextFunction = () => void;
type Middleware = (
req: Request,
res: Response,
next: NextFunction
) => void;
// ミドルウェアの例
const logger: Middleware = (req, res, next) => {
console.log(`${req.url}`);
next();
};
// 認証ミドルウェア(ジェネリクスでリクエストを拡張)
type AuthenticatedRequest = Request & { user: User };
function requireAuth(
handler: (req: AuthenticatedRequest, res: Response) => void
): Middleware {
return (req, res, next) => {
const token = req.headers['authorization'];
if (!token) {
res.status = 401;
return;
}
const user = verifyToken(token);
handler({ ...req, user }, res);
};
}
よくあるエラーと対処法
TypeScriptで関数の型定義を書くときに遭遇しやすいエラーと、その原因・対処法をまとめます。
エラー1: 引数の個数が合わない
Expected N arguments, but got M
function greet(name: string, age: number) {
return `${name} (${age})`;
}
// NG: 引数が足りない
greet('Alice');
// Error: Expected 2 arguments, but got 1.
// 対処法1: オプション引数にする
function greet2(name: string, age?: number) { ... }
// 対処法2: デフォルト値を指定
function greet3(name: string, age: number = 0) { ... }
エラー2: 戻り値の型が合わない
Type X is not assignable to type Y
// NG: string を返しているのに number 型と宣言
function calculate(a: number, b: number): number {
return `${a} + ${b}`;
// Error: Type 'string' is not assignable to type 'number'.
}
// 対処法: 戻り値の型を正しく修正
function calculate2(a: number, b: number): string {
return `${a} + ${b}`;
}
エラー3: オーバーロードの実装シグネチャが不適切
オーバーロードの実装シグネチャエラー
// NG: 実装シグネチャがオーバーロードと互換性がない
function process(input: string): string;
function process(input: number): number;
function process(input: string): string | number {
// Error: 実装シグネチャが number のオーバーロードと互換性がない
return input;
}
// 対処法: 実装シグネチャはすべてのオーバーロードを包含する型にする
function process2(input: string): string;
function process2(input: number): number;
function process2(input: string | number): string | number {
return input;
}
エラー4: ジェネリクスの型パラメータが推論できない
ジェネリクスの推論エラー
// 型パラメータが引数から推論できない場合
function createArray<T>(length: number): T[] {
return new Array<T>(length);
}
// NG: T を推論する情報がない → unknown[] になる
const arr = createArray(3); // unknown[]
// 対処法: 型引数を明示的に指定
const arr2 = createArray<string>(3); // string[]
// または: 引数から推論できるように設計を変える
function createArray2<T>(length: number, fill: T): T[] {
return new Array(length).fill(fill);
}
const arr3 = createArray2(3, ''); // string[] (推論)
エラー5: void コールバックで戻り値を使おうとする
void 型の戻り値を使おうとするエラー
type Callback = () => void;
function execute(cb: Callback) {
const result = cb();
// result の型は void
// NG: void 型は使えない
// if (result) { ... } // Error: void 型は条件式に使えない
// return result; // Error(戻り値型が void 以外の場合)
}
// 対処法: 戻り値を使うなら void 以外の型を指定
type CallbackWithReturn = () => boolean;
function execute2(cb: CallbackWithReturn) {
const result = cb();
if (result) { /* OK */ }
}
エラー6: 分割代入引数の型注釈の間違い
分割代入の型注釈の誤り
// NG: name を string にリネームする構文と解釈される
function greet({ name: string }) {
// string は変数名、name はアクセスできない
console.log(string); // リネームされた変数
}
// OK: 型注釈はオブジェクト全体の後ろに書く
function greet2({ name }: { name: string }) {
console.log(name); // OK
}
| エラーメッセージ |
原因 |
対処法 |
Expected N arguments, but got M |
引数の個数が合わない |
オプション引数(?)またはデフォルト値を使う |
Type X is not assignable to type Y |
戻り値や引数の型が不一致 |
型注釈を修正するか、型アサーションを使う |
No overload matches this call |
オーバーロードに一致するシグネチャがない |
引数の型を確認し、適切なオーバーロードに合わせる |
Parameter implicitly has an any type |
strictモードで引数の型注釈がない |
引数に明示的な型注釈を追加する |
void expression used where value expected |
void型の戻り値を変数に代入しようとした |
関数の戻り値型をvoid以外に変更する |
this implicitly has type any |
thisの型が推論できない |
thisパラメータで明示的に型を指定する |
まとめ
この記事では、TypeScriptの関数に関する型定義を基本から実務レベルまで体系的に解説しました。最後に、各セクションの要点を振り返ります。
| トピック |
ポイント |
| 引数・戻り値の型 |
引数には: Type、戻り値は): Typeで注釈。戻り値は型推論に任せてもOK |
| オプション引数 |
?で省略可能に。デフォルト値を使えばundefinedチェックも不要 |
| レストパラメータ |
...args: T[]で可変長引数。タプル型との組み合わせも強力 |
| 関数型・コールシグネチャ |
シンプルな場合は関数型式、プロパティ付きはコールシグネチャ |
| void / never / undefined |
void=戻り値を使わない、never=終了しない、undefined=undefinedを返す |
| オーバーロード |
引数と戻り値の対応関係を定義。まずUnion型を検討し、必要な場合のみ使う |
| ジェネリクス |
型パラメータで汎用的な関数を実現。extends制約とkeyofを活用 |
| 型ガード関数 |
value is Typeで型を絞り込み。filter()との相性が抜群 |
| thisパラメータ |
最初の引数としてthisの型を宣言。メソッドチェーンにも活用 |
| 非同期関数 |
戻り値はPromise<T>。Awaitedで実質的な型を取得 |
| 高階関数・カリー化 |
関数を返す関数の型定義。ジェネリクスとの組み合わせで柔軟に |
関数の型定義は、TypeScriptの型システムの中でも特に実務で頻繁に使う部分です。基本的な引数・戻り値の型注釈から始めて、徐々にジェネリクスやオーバーロードなどの応用テクニックを身につけていきましょう。