TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 引数と戻り値の型関係

TypeScript
スポンサーリンク

「引数の型」と「戻り値の型」はセットで設計するもの

まず一番大事な前提から。

関数の型は、
「何を受け取って(引数)、何を返すか(戻り値)」
この2つの組み合わせで決まります。

function add(a: number, b: number): number {
  return a + b;
}
TypeScript

この関数の型は、言葉にすると

numbernumber を受け取って、number を返す関数」

です。

TypeScript で関数を設計するときは、
「引数だけ」「戻り値だけ」をバラバラに考えるのではなく、
「この関数は、どんな入力を受け取って、どんな出力を約束するのか」
という“契約”としてセットで考えるのがとても重要です。

ここから、その感覚を具体例で固めていきます。


基本:引数は「受け取れるものの範囲」、戻り値は「約束する結果の範囲」

引数の型は「この関数に渡していい値の範囲」

例えば次の関数を見てください。

function greet(name: string) {
  console.log("こんにちは、" + name);
}
TypeScript

ここで name: string は、

「この関数は、string なら何でも受け取るよ」

という意味です。

"Taro" でも "123" でも、string であればOK。
逆に numbernull は渡せません。

引数の型は、
「この関数が“受け入れることを許可する世界”の境界線
だと思ってください。

戻り値の型は「この関数が“必ず返す”と約束するもの」

次の関数を見てみます。

function getUserName(): string {
  return "Taro";
}
TypeScript

ここで : string は、

「この関数は、呼ばれたら必ず string を返す」

という約束です。

"Taro" でも "Hanako" でもいいけれど、
numbernull を返したら約束違反になります。

戻り値の型は、
「この関数が“外の世界に対して約束する結果の形”」
です。

引数は「受け取る側の都合」、
戻り値は「渡す側の約束」
というイメージを持つと、だいぶ整理されます。


例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つの型にそろえる」関数

例えば、stringnumber を受け取って、
必ず 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(狭い)
戻り値:ParseResultParseSuccess | ParseFailure という union で広い)

です。

契約としては、

「文字列を渡してくれたら、
成功したら ok: truevalue を返すし、
失敗したら ok: falseerror を返す」

というものです。

このパターンでは、
「入力は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

外部からは namenull のこともあるけれど、
アプリの中では「必ず string として扱いたい」とします。

このとき、関数の設計としてはこうできます。

function normalizeUser(raw: RawUser): User {
  return {
    id: raw.id,
    name: raw.name ?? "名無しさん",
  };
}
TypeScript

ここでは、

引数:RawUsernamestring | null
戻り値:Usernamestring

です。

この関数の責任は、

namenull かもしれない世界から、
必ず 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 の関数設計は一気に“気持ちよく”なっていきます。

タイトルとURLをコピーしました