「引数の型」と「戻り値の型」はセットで設計するもの
まず一番大事な前提から。
関数の型は、
「何を受け取って(引数)、何を返すか(戻り値)」
この2つの組み合わせで決まります。
function add(a: number, b: number): number {
return a + b;
}
TypeScriptこの関数の型は、言葉にすると
「number と number を受け取って、number を返す関数」
です。
TypeScript で関数を設計するときは、
「引数だけ」「戻り値だけ」をバラバラに考えるのではなく、
「この関数は、どんな入力を受け取って、どんな出力を約束するのか」
という“契約”としてセットで考えるのがとても重要です。
ここから、その感覚を具体例で固めていきます。
基本:引数は「受け取れるものの範囲」、戻り値は「約束する結果の範囲」
引数の型は「この関数に渡していい値の範囲」
例えば次の関数を見てください。
function greet(name: string) {
console.log("こんにちは、" + name);
}
TypeScriptここで name: string は、
「この関数は、string なら何でも受け取るよ」
という意味です。
"Taro" でも "123" でも、string であればOK。
逆に number や null は渡せません。
引数の型は、
「この関数が“受け入れることを許可する世界”の境界線
だと思ってください。
戻り値の型は「この関数が“必ず返す”と約束するもの」
次の関数を見てみます。
function getUserName(): string {
return "Taro";
}
TypeScriptここで : string は、
「この関数は、呼ばれたら必ず string を返す」
という約束です。
"Taro" でも "Hanako" でもいいけれど、number や null を返したら約束違反になります。
戻り値の型は、
「この関数が“外の世界に対して約束する結果の形”」
です。
引数は「受け取る側の都合」、
戻り値は「渡す側の約束」
というイメージを持つと、だいぶ整理されます。
例1:引数と戻り値の型が「同じ」の関数
変換系の関数は「同じ型を受けて、同じ型を返す」ことが多い
例えば、文字列を整形する関数。
function toUpper(value: string): string {
return value.toUpperCase();
}
TypeScriptここでは、
引数:string
戻り値:string
です。
この関数の契約は、
「どんな文字列を渡してもいい。その代わり、必ず文字列を返す」
です。
同じように、数値を変換する関数もあります。
function double(value: number): number {
return value * 2;
}
TypeScript引数と戻り値が同じ型の関数は、
「同じ世界の中で、値を変形するだけの関数」
と捉えると分かりやすいです。
このとき大事なのは、
「この関数は、string の世界の中だけで完結している」
「number の世界の中だけで完結している」
という意識です。
例2:引数と戻り値の型が「違う」関数
変換の方向がはっきりしているパターン
例えば、文字列を数値に変換する関数。
function toNumber(value: string): number {
return Number(value);
}
TypeScriptここでは、
引数:string
戻り値:number
です。
契約としては、
「文字列を渡してくれたら、それを数値に変換して返す」
です。
逆に、数値を文字列にする関数もあります。
function toString(value: number): string {
return String(value);
}
TypeScript引数と戻り値の型が違うときは、
「この関数は、A の世界から B の世界へ“橋渡し”をしている」
と考えるとよいです。
A → B に変換する関数
B → A に変換する関数
のように、「方向」がはっきりしているのがポイントです。
例3:union型と関数の関係(入力が広くて、出力が狭い)
「いろんな型を受け取って、1つの型にそろえる」関数
例えば、string か number を受け取って、
必ず string にして返す関数を考えます。
function normalizeToString(value: string | number): string {
if (typeof value === "number") {
return value.toString();
}
return value;
}
TypeScriptここでは、
引数:string | number(広い)
戻り値:string(狭い)
です。
契約としては、
「文字列でも数値でもいいから渡して。
どっちにしても、最終的には文字列にして返すよ」
というものです。
このパターンは実務でよく出てきます。
- 入力は「いろんなパターン」がありうる(union 型)
- でも関数の外に出すときには「1つの型」にそろえたい
このとき、
「引数の型は広く、戻り値の型は狭く」
という設計になります。
これはとても TypeScript らしい設計です。
例4:union型と関数の関係(入力が狭くて、出力が広い)
「1つの型を受け取って、結果がいくつかのパターンになりうる」関数
例えば、文字列を解析して「成功」か「失敗」を返す関数を考えます。
type ParseSuccess = {
ok: true;
value: number;
};
type ParseFailure = {
ok: false;
error: string;
};
type ParseResult = ParseSuccess | ParseFailure;
function parseNumber(input: string): ParseResult {
const n = Number(input);
if (Number.isNaN(n)) {
return {
ok: false,
error: "数値に変換できません",
};
}
return {
ok: true,
value: n,
};
}
TypeScriptここでは、
引数:string(狭い)
戻り値:ParseResult(ParseSuccess | ParseFailure という union で広い)
です。
契約としては、
「文字列を渡してくれたら、
成功したら ok: true と value を返すし、
失敗したら ok: false と error を返す」
というものです。
このパターンでは、
「入力は1種類だけど、結果は複数パターンありうる」
という関係になっています。
このとき、呼び出し側は戻り値に対して「型の絞り込み」を行います。
const result = parseNumber("123");
if (result.ok) {
console.log(result.value); // success のときだけ
} else {
console.error(result.error); // failure のときだけ
}
TypeScriptここでのポイントは、
引数の型は「この関数が受け付ける入力の世界」
戻り値の型は「この関数が返しうる結果の世界(パターン)」
という役割を持っている、ということです。
設計の視点1:「関数の責任範囲」を型で表す
引数の型で「どこから先をこの関数の責任にするか」を決める
例えば、ユーザー情報を扱うとします。
type RawUser = {
id: string;
name: string | null;
};
type User = {
id: string;
name: string;
};
TypeScript外部からは name が null のこともあるけれど、
アプリの中では「必ず string として扱いたい」とします。
このとき、関数の設計としてはこうできます。
function normalizeUser(raw: RawUser): User {
return {
id: raw.id,
name: raw.name ?? "名無しさん",
};
}
TypeScriptここでは、
引数:RawUser(name が string | null)
戻り値:User(name が string)
です。
この関数の責任は、
「name が null かもしれない世界から、
必ず string である世界に変換する」
ことです。
つまり、
「この関数を通ったあとは、User として扱っていい」
という境界線を、引数と戻り値の型で表現しています。
こういう「責任の境界」を意識して関数を設計すると、
コード全体がかなり整理されます。
設計の視点2:「安全側に倒す」戻り値の型
「本当は失敗するかもしれない」ことを、戻り値の型に隠さない
例えば、次のような関数を考えます。
function findUserName(id: number): string {
// 実は見つからないこともある…
// 見つからなかったら "" を返しておくか…
return "";
}
TypeScript型だけ見ると、
「number を渡したら、必ず string が返ってくる」
ように見えます。
でも実際には「ユーザーが見つからない」ケースがあり、
そのときに空文字を返してごまかしています。
これは、型と現実がズレている状態です。
TypeScript らしい設計にするなら、
「失敗するかもしれない」という事実を、戻り値の型にちゃんと出す
べきです。
function findUserName(id: number): string | null {
// 見つかったら名前、見つからなかったら null
return null;
}
TypeScriptあるいは、先ほどのように union 型で返してもいいです。
type Found = { ok: true; name: string };
type NotFound = { ok: false };
type FindResult = Found | NotFound;
function findUserName(id: number): FindResult {
return { ok: false };
}
TypeScriptここでのポイントは、
「戻り値の型は、“この関数が本当に返しうる現実”を正直に表す」
ということです。
「本当は null かもしれないのに、型だけ string にしておく」
というのは、未来の自分へのトラップになります。
設計の視点3:関数型として見たときの「引数」と「戻り値」
関数そのものを「型」として扱う
TypeScript では、関数そのものを型として表現できます。
type Formatter = (value: string) => string;
TypeScriptこれは、
「string を受け取って string を返す関数」
という型です。
このときも、
引数の型は「この関数が受け取れる入力の世界」
戻り値の型は「この関数が約束する出力の世界」
という意味を持っています。
例えば、こう使えます。
const toUpper: Formatter = (value) => value.toUpperCase();
const addBrackets: Formatter = (value) => `[${value}]`;
function formatAll(values: string[], formatter: Formatter): string[] {
return values.map((v) => formatter(v));
}
TypeScriptここでは、
Formatter を満たす関数なら何でも渡せるformatAll は「string[] を受け取って string[] を返す」
という関係が、型から読み取れます。
関数を「値」として扱うときも、
「引数と戻り値の型の関係」が、その関数の役割そのもの
になっている、という感覚を持っておいてください。
まとめ:「引数と戻り値の型関係」を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
引数の型は
「この関数が受け入れる入力の世界の範囲」
戻り値の型は
「この関数が外に対して約束する結果の世界」
そして、
入力の世界を広くして、出力の世界を狭くする関数は
「いろんなものを受け取って、1つの形にそろえる関数」
入力の世界を狭くして、出力の世界を広くする関数は
「1つの入力から、いくつかの結果パターンを返しうる関数」
関数を設計するときは、
「この関数は、どんな世界からどんな世界へ橋をかけたいのか?」
を一度立ち止まって考えてみてください。
その答えを、
引数の型と戻り値の型の関係として表現できるようになると、
TypeScript の関数設計は一気に“気持ちよく”なっていきます。
