- TypeScript
TypeScript関数の型定義まとめ|引数・戻り値・アロー関数・オーバーロード
TypeScriptの関数の型定義とは、引数の後ろに「: 型」、引数リストの閉じかっこの後ろに「: 戻り値の型」を書いて、関数の入口と出口を決めることです。 関数宣言でもアロー関数でも、書く場所は同じです。違いが出るのは「関数そのものの型を変数や引数に付けるとき」で、ここでは (a: number) => string という矢印つきの書き方を使います。
この記事では、関数宣言・関数式・アロー関数・メソッド・コールバックの5つの書き方を1枚の早見表にまとめ、すべてコピペできるコードと実行結果で並べます。さらに、オーバーロードで出る「No overload matches this call」を実際に再現して直す手順、ユニオン型やジェネリクスで置き換えたほうがよい場面、void・undefined・neverの違い、戻り値voidのコールバックに値を返せる理由まで、TypeScript 7.0.2で出力したエラー文つきで解説します。
TypeScript関数の型定義の早見表
まずはこの表の「書き方」をそのまま写せば、ほとんどの関数に型を付けられます。
| 種類 | 書き方 | ポイント |
|---|---|---|
| 関数宣言 | function add(a: number, b: number): number { ... } |
引数の後ろと、かっこの後ろに型を書く |
| 関数式 | const sub = function (a: number, b: number): number { ... }; |
書く場所は関数宣言と同じ |
| アロー関数 | const mul = (a: number, b: number): number => a * b; |
戻り値の型は => の前に書く |
| 関数の型(型エイリアス) | type BinaryOp = (a: number, b: number) => number; |
型の中では => の後ろが戻り値 |
| 型を付けた変数 | const div: BinaryOp = (a, b) => a / b; |
引数の型は書かなくても推論される |
| メソッド | const calc = { double(n: number): number { ... } }; |
オブジェクトの中でも書く場所は同じ |
| メソッドの型 | interface Counter { inc(step?: number): number; reset: () => void } |
メソッド形式とプロパティ形式の2通り |
| コールバック引数 | function each(items: string[], fn: (item: string, index: number) => void): void |
引数の型として関数の型を書く |
| 省略できる引数 | function greet(name: string, title?: string) |
型は string | undefined になる |
| 既定値つき引数 | function withTax(amount: number, rate = 0.1) |
既定値から型が推論される |
| 残余引数 | function sum(...nums: number[]) |
配列の型で書く |
| 分割代入の引数 | function f({ name, age = 20 }: Input) |
型は右側にまとめて書く |
| 非同期関数 | async function load(id: number): Promise<string> |
戻り値は必ず Promise<T> |
| ジェネリクス | function first<T>(items: T[]): T | undefined |
アロー関数は <T,>(items: T[]) => ... |
| オーバーロード | function f(v: string): number; を実装の上に並べる |
実装のシグネチャは外から見えない |
| thisの型 | function describe(this: User, prefix: string) |
先頭の this は実際の引数に数えない |
戻り値の型でよく迷う3つは、次のように使い分けます。
| 型 | 意味 | 使う場面 |
|---|---|---|
void |
戻り値を使わない | ログ出力、イベント処理、コールバックの戻り値 |
undefined |
undefined という値を返す | 「見つからなければ undefined」を表す string | undefined など |
never |
関数から戻ってこない | 必ず例外を投げる関数、全パターンを書き切ったかの検査 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript関数の型定義まとめ|引数・戻り値・アロー関数・オーバーロード」
検証した環境とコマンド
この記事のコードと出力は、次の環境で実際に動かしたものです。
| 項目 | 内容 |
|---|---|
| TypeScript | 7.0.2(npm i typescript で入った版) |
| Node.js | v22.22.2 |
| 型チェック | npx tsc --noEmit --strict --target es2022 ファイル名.ts |
| 実行 | node ファイル名.ts |
| 推論された型の確認 | npx tsc --declaration --emitDeclarationOnly で出力した .d.ts |
エラー文は tsc が出したものをそのまま載せています。 エディタ上の表示と行番号・文面が同じになるので、手元で試すときの照合に使えます。
関数宣言・関数式・アロー関数・メソッド・コールバックの型を書く
5つの書き方を1ファイルにまとめました。そのまま貼って動きます。
// basic.ts
// 1. 関数宣言
function add(a: number, b: number): number {
return a + b;
}
// 2. 関数式
const sub = function (a: number, b: number): number {
return a - b;
};
// 3. アロー関数
const mul = (a: number, b: number): number => a * b;
// 4. 関数の型を先に作り、変数に付ける(引数の型は書かなくてよい)
type BinaryOp = (a: number, b: number) => number;
const div: BinaryOp = (a, b) => a / b;
// 5. メソッド(オブジェクトの中の関数)
const calc = {
double(n: number): number {
return n * 2;
},
triple: (n: number): number => n * 3,
};
// 6. コールバックを受け取る関数
function each(items: string[], fn: (item: string, index: number) => void): void {
items.forEach((item, i) => fn(item, i));
}
console.log(add(1, 2), sub(5, 3), mul(2, 4), div(9, 3));
console.log(calc.double(5), calc.triple(5));
each(["a", "b"], (item, i) => console.log(i, item));
実行結果:
3 2 8 3
10 15
0 a
1 b
覚えておきたいのは、「関数を書くとき」と「関数の型を書くとき」で戻り値の記号が変わる点です。
| 場面 | 戻り値の書き方 | 例 |
|---|---|---|
| 関数を書く(宣言・式・アロー) | かっこの後ろに : 型 |
(a: number): string => String(a) |
| 関数の型を書く(type・引数の型) | => 型 |
type F = (a: number) => string |
| interface の中のメソッド | 名前(引数): 型 |
interface A { f(a: number): string } |
4番の div のように、変数に関数の型を付けておけば、右側の関数では引数の型を省略できます。左側の型から引数の型が決まるためで、これを文脈からの推論と呼びます。コールバックを書くときに (item, i) => ... と型なしで書けるのも同じ仕組みです。
引数の型を書く(省略・既定値・残余引数・分割代入)
引数まわりは4つのパターンを押さえれば足ります。
// args.ts
// 省略できる引数(?): 型は string | undefined になる
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
// 既定値つき引数: 型は既定値から number と推論される
function withTax(amount: number, rate = 0.1): number {
return Math.round(amount * (1 + rate));
}
// 残余引数(...): 配列の型で書く
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// オブジェクトで受け取る(分割代入): 型は右側にまとめて書く
type CreateUserInput = { name: string; age?: number; tags?: string[] };
function createUser({ name, age = 20, tags = [] }: CreateUserInput): string {
return `${name}(${age}) [${tags.join(",")}]`;
}
// 戻り値がタプル・Promise
function minMax(nums: number[]): [number, number] {
return [Math.min(...nums), Math.max(...nums)];
}
async function loadTitle(id: number): Promise<string> {
return `記事${id}`;
}
console.log(greet("佐藤"), "/", greet("佐藤", "Dr."));
console.log(withTax(1000), withTax(1000, 0.08));
console.log(sum(), sum(1, 2, 3));
console.log(createUser({ name: "鈴木" }), createUser({ name: "田中", age: 31, tags: ["a", "b"] }));
console.log(minMax([3, 9, 1]));
loadTitle(7).then((t) => console.log(t));
実行結果:
佐藤 / Dr. 佐藤
1100 1080
0 6
鈴木(20) [] 田中(31) [a,b]
[ 1, 9 ]
記事7
| やりたいこと | 書き方 | 関数の中での型 | 補足 |
|---|---|---|---|
| 引数を省略できるようにする | title?: string |
string | undefined |
必須の引数より後ろに置く |
| 省略時の値を決める | rate = 0.1 |
number |
? と既定値を同時に書くと TS1015 |
| 何個でも受け取る | ...nums: number[] |
number[] |
引数リストの最後に1つだけ |
| 名前つきで受け取る | { name, age = 20 }: Input |
各プロパティの型 | 引数が3つを超えたらこの形が読みやすい |
| null も受け取る | title: string | null |
string | null |
省略はできない(null を渡す必要がある) |
分割代入の引数では { name: string } と書いても型にはなりません。 分割代入の中のコロンは「別名を付ける」という JavaScript の構文だからです。型は必ず }: 型 の位置にまとめて書きます。
戻り値の型は書くべきか(推論される型の一覧)
戻り値の型は、書かなくても TypeScript が推論します。 次のコードから .d.ts を出力して、推論された型を確かめました。
// infer.ts
export function add(a: number, b: number) {
return a + b;
}
export const toLabel = (n: number) => `No.${n}`;
export const pick = (flag: boolean) => (flag ? "yes" : 0);
export async function load(id: number) {
return { id, title: "記事" };
}
export function log(message: string) {
console.log(message);
}
export function fail(message: string) {
throw new Error(message);
}
export const fail2 = (message: string) => {
throw new Error(message);
};
出力された型(.d.ts):
export declare function add(a: number, b: number): number;
export declare const toLabel: (n: number) => string;
export declare const pick: (flag: boolean) => "yes" | 0;
export declare function load(id: number): Promise<{
id: number;
title: string;
}>;
export declare function log(message: string): void;
export declare function fail(message: string): void;
export declare const fail2: (message: string) => never;
ここから読み取れることを表にします。
| 書いたコード | 推論された戻り値 | 気づき |
|---|---|---|
return a + b |
number |
素直に推論される |
flag ? "yes" : 0 |
"yes" | 0 |
分岐があるとユニオンになる |
| async 関数 | Promise<{ id: number; title: string }> |
自動で Promise に包まれる |
| return がない | void |
undefined ではなく void |
| 必ず throw する関数宣言 | void |
never にはならない |
| 必ず throw するアロー関数 | never |
関数式・アロー関数は never と推論される |
必ず例外を投げる関数は、関数宣言だと void、アロー関数だと never と推論されます。 関数宣言で never にしたいときは、function fail(message: string): never と自分で書きます。
では、戻り値の型はいつ書くとよいでしょうか。判断の目安は次のとおりです。
| 場面 | 戻り値の型 | 理由 |
|---|---|---|
| 1〜2行の小さな関数、コールバック | 省略でよい | 推論で十分に読める |
| 他のファイルから使う関数(export) | 書く | 関数の約束が見出しのように読める。実装を変えたときに呼び出し側ではなく関数の中でエラーが出る |
| 分岐が多い関数 | 書く | 意図しないユニオンになったことにすぐ気づける |
| 必ず throw する関数宣言 | never と書く |
推論では void になる |
| 再帰する関数 | 書く | 自分自身を return で呼ぶと推論が循環し、TS7023 で止まる |
関数の型でよく出るエラーと直し方
最初に出会いやすいエラーを1ファイルで再現しました。
// err_basic.ts
function add(a: number, b: number): number {
return a + b;
}
add(1);
add(1, 2, 3);
add("1", 2);
function toLabel(n: number): string {
return n;
}
function show(value) {
console.log(value);
}
function greet(title?: string, name: string): string {
return `${title} ${name}`;
}
export {};
tsc の出力:
err_basic.ts(4,1): error TS2554: Expected 2 arguments, but got 1.
err_basic.ts(5,11): error TS2554: Expected 2 arguments, but got 3.
err_basic.ts(6,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
err_basic.ts(8,3): error TS2322: Type 'number' is not assignable to type 'string'.
err_basic.ts(10,15): error TS7006: Parameter 'value' implicitly has an 'any' type.
err_basic.ts(13,32): error TS1016: A required parameter cannot follow an optional parameter.
| コード | 意味 | 直し方 |
|---|---|---|
| TS2554 | 引数の個数が合わない | 呼び出しを直す。省略したいなら b?: number か既定値にする |
| TS2345 | 引数の型が合わない | Number("1") のように変換してから渡す |
| TS2322 | return した値が戻り値の型と合わない | return String(n) にするか、戻り値の型を見直す |
| TS7006 | 引数に型がなく any になる | value: unknown など型を書く |
| TS1016 | 省略できる引数の後ろに必須の引数がある | 必須の引数を前に並べ替える |
関数の型を作る(typeとinterface、メソッド形式とプロパティ形式)
関数の型は、3通りの書き方ができます。
// 1. 型エイリアス(いちばんよく使う)
type BinaryOp = (a: number, b: number) => number;
// 2. 呼び出しシグネチャ(関数にプロパティも持たせたいとき)
interface Formatter {
(value: number): string;
unit: string;
}
// 3. オブジェクトの型の中のメソッド
interface Counter {
inc(step?: number): number; // メソッド形式
reset: () => void; // プロパティ形式
}
メソッド形式とプロパティ形式は、引数の型のチェックの厳しさが違います。 次のコードで確かめました。
// err_callback.ts(後半)
type Handler1 = { handle: (v: string | number) => void }; // プロパティ形式
type Handler2 = { handle(v: string | number): void }; // メソッド形式
const h1: Handler1 = { handle: (v: string) => console.log(v.trim()) };
const h2: Handler2 = { handle: (v: string) => console.log(v.trim()) };
err_callback.ts(10,24): error TS2322: Type '(v: string) => void' is not assignable to type '(v: string | number) => void'.
Types of parameters 'v' and 'v' are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
エラーになったのはプロパティ形式の h1 だけです。handle には数値も渡される約束なのに、代入した関数は文字列しか扱えないので、プロパティ形式ではきちんと止まります。メソッド形式の h2 は同じ代入が通り、数値が渡されると v.trim() で実行時エラーになります。新しく型を作るときは、チェックが厳しいプロパティ形式(handle: (v) => void)を選ぶと安心です。
コールバック関数の型を書く
コールバックは「引数の型として関数の型を書く」だけです。ここで押さえたいのは、渡す関数の引数は、型より少なくてもよいが、多いとエラーになるという点です。
// err_callback.ts(前半)
function each(items: string[], fn: (item: string, index: number) => void): void {
items.forEach((item, i) => fn(item, i));
}
each(["a"], (item) => console.log(item)); // 引数が少ないのはOK
each(["a"], (item, index, all) => console.log(item)); // 引数が多い
each(["a"], (item: number) => console.log(item)); // 引数の型が違う
err_callback.ts(5,13): error TS2345: Argument of type '(item: any, index: any, all: any) => void' is not assignable to parameter of type '(item: string, index: number) => void'.
Target signature provides too few arguments. Expected 3 or more, but got 2.
err_callback.ts(6,13): error TS2345: Argument of type '(item: number) => void' is not assignable to parameter of type '(item: string, index: number) => void'.
Types of parameters 'item' and 'item' are incompatible.
Type 'string' is not assignable to type 'number'.
(5行目では、あわせて各引数に TS7006 も出ます。上の出力では省いています。)
| 渡した関数 | 結果 | 理由 |
|---|---|---|
(item) => ... |
通る | 使わない引数は受け取らなくてよい(forEach で index を省けるのと同じ) |
(item, index, all) => ... |
TS2345 | 呼び出す側は2つしか渡さないので、3つ目は届かない |
(item: number) => ... |
TS2345 | 実際には string が渡される |
このため、コールバックの型で引数に ? を付ける必要はほとんどありません。fn: (item: string, index?: number) => void と書くと「index が渡されないことがある」という意味になり、コールバックの中で undefined の確認が必要になります。
void・undefined・neverの違いを実行結果で確かめる
void は「戻り値を使わない」、undefined は「undefined という値」、never は「戻ってこない」です。 動くコードで確かめます。
// void.ts
// void: 戻り値を使わない関数
function log(message: string): void {
console.log(message);
}
const r = log("hello");
console.log("log の戻り値:", r);
// 戻り値 void の関数型には、値を返す関数も代入できる
type VoidCallback = () => void;
const cb: VoidCallback = () => 123;
const v = cb(); // v の型は void(number ではない)
console.log("cb の戻り値:", v);
// このおかげで forEach に push を直接渡せる(push は number を返す)
const src = [1, 2, 3];
const dst: number[] = [];
src.forEach((n) => dst.push(n * 10));
console.log(dst);
// undefined: 「undefined という値」を返す
function findName(id: number): string | undefined {
return id === 1 ? "佐藤" : undefined;
}
console.log(findName(1), findName(2));
// never: 戻ってこない関数(必ず例外を投げる、または無限ループ)
function fail(message: string): never {
throw new Error(message);
}
type Status = "draft" | "published";
function label(status: Status): string {
switch (status) {
case "draft":
return "下書き";
case "published":
return "公開中";
default: {
const rest: never = status; // 全部の場合を書いていれば never になる
return fail(`未対応の値: ${rest}`);
}
}
}
console.log(label("draft"), label("published"));
try {
fail("止めます");
} catch (e) {
console.log("catch:", (e as Error).message);
}
実行結果:
hello
log の戻り値: undefined
cb の戻り値: 123
[ 10, 20, 30 ]
佐藤 undefined
下書き 公開中
catch: 止めます
戻り値voidのコールバックに値を返せる理由
出力の3行目に注目してください。cb の型は () => void ですが、実行すると 123 が返っています。型チェックも通ります。
これは TypeScript が意図して用意している動きです。公式ハンドブックの Functions の章には、戻り値が void の関数型は「何も返さないこと」を強制するものではなく、実装が値を返しても無視される、という趣旨の説明があります(今回ページを開いて確認しました)。この決まりがあるおかげで、src.forEach((n) => dst.push(n * 10)) のように、値を返す関数をそのままコールバックに渡せます。もし値を返す関数を渡せなかったら、毎回 { dst.push(n * 10); } と波かっこで包む必要があります。
ただし、次の2点は区別されます。
| 書き方 | 値を return | 理由 |
|---|---|---|
const cb: VoidCallback = () => 123;(関数型を付けた変数・コールバック) |
できる | 「戻り値を使わない」という約束なので、何が返っても支障がない |
function log(): void { return 123; }(関数そのものに void と書く) |
エラー(TS2322) | 自分で「返さない」と宣言している |
void・undefined・neverで出るエラー
// err_void.ts
function log(message: string): void {
return message.length;
}
type VoidCallback = () => void;
const cb: VoidCallback = () => 123;
const v = cb();
const n: number = v;
if (v) {
console.log("truthy");
}
function noReturn(): undefined {}
function noReturn2(): number {}
function fail(message: string): never {
console.log(message);
}
type Status = "draft" | "published" | "archived";
function label(status: Status): string {
switch (status) {
case "draft":
return "下書き";
case "published":
return "公開中";
default: {
const rest: never = status;
return rest;
}
}
}
export {};
err_void.ts(2,3): error TS2322: Type 'number' is not assignable to type 'void'.
err_void.ts(7,7): error TS2322: Type 'void' is not assignable to type 'number'.
err_void.ts(8,5): error TS1345: An expression of type 'void' cannot be tested for truthiness.
err_void.ts(12,23): error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
err_void.ts(13,33): error TS2534: A function returning 'never' cannot have a reachable end point.
err_void.ts(24,13): error TS2322: Type '"archived"' is not assignable to type 'never'.
| 行 | 何が起きたか | 読み取れること |
|---|---|---|
| 2行目 | void の関数で値を return | 関数そのものに void と書いたら値は返せない |
| 7・8行目 | void の戻り値を number に代入、if で判定 | 実行時に 123 が入っていても、型の上では使えない。これが「無視される」の意味 |
| 11行目 | (): undefined {} |
エラーなし。return を書かなくても undefined を返す関数として通る |
| 12行目 | (): number {} |
値を返す約束なのに return がない |
| 13行目 | never なのに最後まで到達する | never の関数は throw か無限ループで終える |
| 24行目 | Status に "archived" を足した | switch に書き足し忘れた値が、エラー文に名前で出る |
最後の24行目が never のいちばん便利な使い方です。ユニオン型に値を足したとき、対応を書き忘れた場所を型チェックが教えてくれます。
オーバーロードの書き方と「No overload matches this call」の直し方
オーバーロードは、同じ関数に「引数と戻り値の組み合わせ」を複数用意する書き方です。 実装の上に、本体のないシグネチャを並べます。
// overload.ts
// オーバーロードシグネチャ(呼び出す側に見える型)
function format(value: string): string;
function format(value: Date): string;
function format(value: number, digits: number): string;
// 実装シグネチャ(呼び出す側からは見えない)
function format(value: string | Date | number, digits?: number): string {
if (typeof value === "string") return value.trim();
if (value instanceof Date) return value.toISOString().slice(0, 10);
return value.toFixed(digits);
}
console.log(format(" abc "));
console.log(format(new Date("2026-10-03T00:00:00Z")));
console.log(format(3.14159, 2));
実行結果:
abc
2026-10-03
3.14
エラーを再現する
この format に対して、次の3つの呼び出しを書いてみます。
// err_overload.ts(呼び出し部分。format の定義は上と同じ)
format(true);
function show(input: string | Date) {
return format(input);
}
format(3.14159);
err_overload.ts(10,8): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'boolean' is not assignable to parameter of type 'Date'.
err_overload.ts(13,17): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | Date' is not assignable to parameter of type 'Date'.
Type 'string' is not assignable to type 'Date'.
err_overload.ts(16,8): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'number' is not assignable to parameter of type 'Date'.
3つとも TS2769「No overload matches this call」(この呼び出しに合うオーバーロードがない)です。原因はそれぞれ違います。
| 呼び出し | 原因 | 気づきにくい点 |
|---|---|---|
format(true) |
boolean を受け取るシグネチャがない | 素直な型の不一致 |
format(input)(input は string | Date) |
ユニオン型のまま渡した。string 用と Date 用は別々のシグネチャで、両方を一度に満たすものがない | string も Date も個別には受け取れるのに止まる |
format(3.14159) |
number 用は引数が2つ必要 | エラー文には「Date に代入できない」と出る。引数の個数が合う最後のシグネチャと比べた結果が表示されるため |
2つ目と3つ目は、エラー文だけを読むと原因を取り違えやすい例です。TS2769 が出たら、エラー文の最後の行より先に、シグネチャの一覧と見比べるのが近道です。
直す手順
| 手順 | すること |
|---|---|
| 1 | 関数名にカーソルを合わせる(または定義へ移動する)と、オーバーロードシグネチャの一覧が見える |
| 2 | 渡している引数の「個数」が合うシグネチャがあるか確かめる |
| 3 | 個数が合うシグネチャの中で、各引数の「型」が合うものがあるか確かめる |
| 4 | 渡している値がユニオン型なら、1つのシグネチャでユニオン全体を受け取れるか確かめる |
| 5 | 実装シグネチャ(本体が付いている行)は候補に入らないことを思い出す |
ユニオン型を渡したい場合の直し方は2つあります。
// fix_overload.ts(format の定義は上と同じ)
// 直し方A: 呼び出す側で型を絞ってから渡す
function showA(input: string | Date) {
return typeof input === "string" ? format(input) : format(input);
}
// 直し方B: ユニオンを受け取るシグネチャにまとめる
function format2(value: string | Date): string;
function format2(value: number, digits: number): string;
function format2(value: string | Date | number, digits?: number): string {
if (typeof value === "string") return value.trim();
if (value instanceof Date) return value.toISOString().slice(0, 10);
return value.toFixed(digits);
}
function showB(input: string | Date) {
return format2(input);
}
console.log(showA(" a "), showB(new Date("2026-10-03T00:00:00Z")), format2(1.005, 1));
実行結果:
a 2026-10-03 1.0
直し方Aは関数を変えられないとき(ライブラリの関数など)に使います。自分の関数なら、直し方Bのようにシグネチャをまとめるほうが、呼び出す側が楽になります。
オーバーロードを書くときに出るエラー
// err_overload2.ts
function parse(value: string): number;
function parse(value: number): string;
function parse(value: string): number | string {
return typeof value === "string" ? Number(value) : String(value);
}
function size(value: string): number;
function size(value: unknown[]): number;
export {};
err_overload2.ts(2,10): error TS2394: This overload signature is not compatible with its implementation signature.
err_overload2.ts(8,10): error TS2391: Function implementation is missing or not immediately following the declaration.
| コード | 原因 | 直し方 |
|---|---|---|
| TS2394 | 実装シグネチャが、2つ目のシグネチャ(number を受け取る)を含んでいない | 実装の引数を value: string | number にする |
| TS2391 | シグネチャだけで実装がない | シグネチャの直後に実装を書く |
もう1つ、シグネチャは上から順に試されることも覚えておきます。
// err_overload3.ts
// 広いシグネチャを先に書くと、後ろのシグネチャが選ばれなくなる
function toArray(value: unknown): unknown[];
function toArray(value: string): string[];
function toArray(value: unknown): unknown[] {
return [value];
}
const r: string[] = toArray("a");
export {};
err_overload3.ts(7,7): error TS2322: Type 'unknown[]' is not assignable to type 'string[]'.
Type 'unknown' is not assignable to type 'string'.
文字列を渡したのに、先に書いた unknown 用のシグネチャが選ばれました。具体的なシグネチャを上、広いシグネチャを下に並べると、期待どおり string[] になります。
ユニオン型・ジェネリクスで置き換える場面
公式ハンドブックには「可能なときは、オーバーロードよりユニオン型の引数を選ぶ」という指針があります(今回ページを開いて確認しました)。先ほどのように、ユニオン型のまま渡せなくなることがあるためです。置き換えの目安を表にします。
| 状況 | 向いている書き方 | 例 |
|---|---|---|
| 引数の型が違うだけで、戻り値の型は同じ | ユニオン型 | len(value: string | unknown[]): number |
| 引数の個数が違うだけ | 省略できる引数・既定値 | pad(value: number, width = 3) |
| 引数の型がそのまま戻り値の型に出る | ジェネリクス | wrap<T>(value: T): T[] |
| 引数の型によって戻り値の型が変わる | オーバーロード | parse(string): number と parse(number): string |
| 引数の組み合わせに決まりがある(Aのときだけ2つ目が必須) | オーバーロード | format(number, digits) |
// overload_alt.ts
// 代替1: ユニオン型(引数の型が違うだけで、戻り値の型は同じとき)
function len(value: string | unknown[]): number {
return value.length;
}
function show(input: string | number[]) {
return len(input); // ユニオンのまま渡せる
}
// 代替2: 省略できる引数(引数の個数が違うだけのとき)
function pad(value: number, width = 3): string {
return String(value).padStart(width, "0");
}
// 代替3: ジェネリクス(引数の型がそのまま戻り値の型に出るとき)
function wrap<T>(value: T): T[] {
return [value];
}
export const a = wrap("x");
export const b = wrap(1);
// オーバーロードが合う場面: 引数の型によって戻り値の型が変わる
function parse(value: string): number;
function parse(value: number): string;
function parse(value: string | number): number | string {
return typeof value === "string" ? Number(value) : String(value);
}
export const n = parse("42");
export const s = parse(42);
// アロー関数・関数式でオーバーロードしたいときは、呼び出しシグネチャを並べた型を使う
type Parse = {
(value: string): number;
(value: number): string;
};
export const parse2: Parse = (value: any): any =>
typeof value === "string" ? Number(value) : String(value);
console.log(len("abc"), len([1, 2]), show([1, 2, 3]));
console.log(pad(7), pad(7, 5));
console.log(a, b, n, s, parse2("5"), parse2(5));
実行結果と、.d.ts に出力された型:
3 2 3
007 00007
[ 'x' ] [ 1 ] 42 42 5 5
export declare const a: string[];
export declare const b: number[];
export declare const n: number;
export declare const s: string;
parse("42") は number、parse(42) は string と、渡した型に応じて戻り値の型が切り替わっています。これがオーバーロードの出番です。
アロー関数にはオーバーロードシグネチャを直接並べられないので、type Parse = { (...): ...; (...): ...; } のように呼び出しシグネチャを並べた型を作って変数に付けます。この書き方では実装側の引数と戻り値を any にする必要があり、関数の中の型チェックがゆるくなります。オーバーロードが必要なら、関数宣言で書くほうが扱いやすくなります。
thisの型を書く
関数の中で this を使うときは、引数リストの先頭に this: 型 と書きます。 これは型のためだけの特別な引数で、呼び出すときの引数には数えません。
// this.ts
type User = { name: string };
// 先頭の this は「呼び出すときの this の型」を表す特別な引数(実際の引数には数えない)
function describe(this: User, prefix: string): string {
return `${prefix}: ${this.name}`;
}
const user = { name: "佐藤", describe };
console.log(user.describe("会員"));
console.log(describe.call({ name: "鈴木" }, "会員"));
実行結果:
会員: 佐藤
会員: 鈴木
this の型を書いておくと、this が合わない呼び出しを型チェックが止めてくれます。
// err_this.ts
type User = { name: string };
function describe(this: User, prefix: string): string {
return `${prefix}: ${this.name}`;
}
describe("会員");
const counter = {
count: 0,
inc() {
return ++this.count;
},
};
function untyped() {
return this.count;
}
export {};
err_this.ts(5,1): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'.
err_this.ts(14,10): error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
| 場面 | this の型 | 補足 |
|---|---|---|
オブジェクトリテラルのメソッド(inc() { ... }) |
そのオブジェクトの型に推論される | 上の counter.inc はエラーなし |
| クラスのメソッド | そのクラスのインスタンス | 書かなくてよい |
| 単独の function | 書かないと TS2683 | this: 型 を先頭に書く |
| アロー関数 | 自分の this を持たない(外側の this をそのまま使う) | this: 型 は書けない。公式ハンドブックでも this の型を使うときは function で書くよう案内されている |
ジェネリクス関数と、関数から型を取り出す書き方
引数の型と戻り値の型を結び付けたいときは、ジェネリクス(型を引数のように受け取る仕組み)を使います。ここでは関数に関わる書き方だけを載せます。
// generic.ts
function first<T>(items: T[]): T | undefined {
return items[0];
}
const last = <T,>(items: T[]): T | undefined => items[items.length - 1];
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 関数からその型を取り出す
function createUser(name: string, age: number) {
return { id: 1, name, age };
}
export type Args = Parameters<typeof createUser>; // [name: string, age: number]
export type User = ReturnType<typeof createUser>; // { id: number; name: string; age: number }
export const f = first(["a", "b"]);
export const l = last([1, 2, 3]);
export const p = prop({ id: 1, title: "記事" }, "title");
console.log(f, l, p);
実行結果と推論された型:
a 3 記事
export declare const f: string | undefined;
export declare const l: number | undefined;
export declare const p: string;
| やりたいこと | 書き方 |
|---|---|
| 関数宣言をジェネリクスにする | function first<T>(items: T[]): T | undefined |
| アロー関数をジェネリクスにする | const last = <T,>(items: T[]) => ...(.tsx では <T,> とカンマを入れると JSX と区別される) |
| 関数の型を取り出す | typeof createUser |
| 引数の型をタプルで取り出す | Parameters<typeof createUser> |
| 戻り値の型を取り出す | ReturnType<typeof createUser> |
| async 関数の中身の型を取り出す | Awaited<ReturnType<typeof load>> |
関数の型定義チェックリスト
| 確認すること | 目安 |
|---|---|
| 引数すべてに型があるか | strict では型なしの引数が TS7006 になる |
| 省略できる引数は必須の引数より後ろか | 逆だと TS1016 |
| export する関数に戻り値の型を書いたか | 実装を変えたときに関数の中でエラーが出る |
| 必ず throw する関数宣言に never を書いたか | 推論では void になる |
| コールバックの戻り値は void にしたか | 値を返す関数もそのまま渡せる |
| オブジェクトの型の中の関数はプロパティ形式か | 引数の型が厳しく検査される |
| オーバーロードはユニオン型・既定値・ジェネリクスで書けないか | 戻り値の型が変わるときだけオーバーロード |
| オーバーロードは具体的なシグネチャが上か | 上から順に試される |
this を使う単独の function に this: 型 を書いたか |
書かないと TS2683 |
よくある質問
Q. アロー関数と関数宣言で、型の付け方に違いはありますか?
A. 引数と戻り値の型を書く場所は同じです。違いは3つあります。アロー関数はオーバーロードシグネチャを直接並べられないこと、this: 型 を書けないこと、必ず throw する場合の戻り値の推論が never になること(関数宣言は void)です。
Q. 戻り値の型は毎回書いたほうがよいですか? A. 小さな関数やコールバックは推論に任せて問題ありません。他のファイルから使う関数や分岐の多い関数は、書いておくと意図しない型の変化にすぐ気づけます。
Q. 「No overload matches this call」が出たら、まず何を見ればよいですか? A. 関数のシグネチャ一覧です。渡している引数の個数と型が、どれか1つのシグネチャに丸ごと合っているかを確かめます。ユニオン型の値を渡している場合は、型を絞ってから渡すか、ユニオン型を受け取るシグネチャにまとめると解消します。
Q. void と undefined はどちらを使えばよいですか?
A. 戻り値を使わない関数は void、呼び出す側が「undefined かどうか」を調べる関数は string | undefined のように undefined を含めた型にします。コールバックの戻り値は void にしておくと、値を返す関数も渡せて使いやすくなります。
まとめ
- 関数を書くときは「引数: 型」と「): 戻り値の型」、関数の型を書くときは「(引数: 型) => 戻り値の型」
- 引数は
?(省略)・既定値・...(残余)・分割代入の4パターンで、省略できる引数は後ろに置く - void は「戻り値を使わない」、undefined は「値」、never は「戻ってこない」。戻り値 void の関数型には値を返す関数も代入でき、返した値は型の上では使えない
- TS2769「No overload matches this call」は、個数と型が丸ごと合うシグネチャがないときに出る。ユニオン型のまま渡すと起きやすい
- オーバーロードは「引数の型で戻り値の型が変わる」ときに使い、それ以外はユニオン型・既定値・ジェネリクスで書く
- オブジェクトの型の中の関数はプロパティ形式、単独の function で this を使うなら
this: 型を書く
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript関数の型定義まとめ|引数・戻り値・アロー関数・オーバーロード」
株式会社RJCでは、TypeScriptの関数の型定義を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。