---
title: "関数型プログラミングとは？純粋関数・不変性の基本とメリット・デメリットをTypeScriptの実例で解説"
url: "https://www.issoh.co.jp/tech/details/4841/"
published: 2025-01-15
updated: 2026-11-04
categories: ["プログラミング言語"]
publisher: "株式会社一創"
---

# 関数型プログラミングとは？純粋関数・不変性の基本とメリット・デメリットをTypeScriptの実例で解説

関数型プログラミング（Functional Programming、FP）は、変数を書き換えて手順を進めるのではなく、値を受け取って新しい値を返す関数を組み合わせて処理を組み立てる書き方です。中心にあるのは、純粋関数・イミュータブル（不変）なデータ・高階関数・関数合成の4つです。

この記事のコードは、TypeScript 7.0.2（`tsc --strict --target es2023`）でコンパイルし、Node.js v26.5.0で実行したものです（性能の計測だけはJavaScriptの`.mjs`として実行）。メリット・デメリット、言語の一覧と主流にならない理由、関数型ドメインモデリング、関数型で書いてかえって遅くなる失敗パターンまで扱います。

## まとめ：関数型プログラミングの要点

- 関数型プログラミングは、状態の書き換えではなく、関数の適用と合成で計算を表すプログラミングパラダイムです。
- 純粋関数は、同じ引数なら必ず同じ値を返し、外の状態を変えません。テストは入力と出力を比べるだけで済みます。
- 向くのは、データの変換・集計、業務ルールの判定、画面の状態計算です。I/Oが中心の処理や、大量データを毎回コピーする処理には向きません。
- TypeScriptの`readonly`は型検査のときだけ働き、出力されたJavaScriptでは書き換えが通ります。`Object.freeze`も凍結するのは1階層だけです。
- Stack Overflowの2025年調査では、選択肢にある関数型中心の言語で最も利用率が高いElixirでも2.7%です。関数型の書き方そのものは、TypeScript・Java・C#・Pythonなどの主要言語に取り込まれて広く使われています。
- 業務ロジックでは、状態ごとに型を分け、失敗を例外でなくResult型の値で返すと、未検証のデータを後続の処理へ渡す誤りをコンパイル時に止められます。

## 関数型プログラミングの定義と命令型・オブジェクト指向との違い

命令型プログラミングは「変数にこの値を入れ、次にこの値を足す」という手順の列でプログラムを書きます。関数型プログラミングは、入力から出力を求める関数を定義し、それを組み合わせた式として計算を書きます。

### forループをmap・filter・reduceに置き換える例

在庫のある商品だけを選び、税込価格の合計を出す処理を、2つの書き方で比べます。

```
type Item = { name: string; price: number; inStock: boolean };
const items: Item[] = [
  { name: "pen", price: 1200, inStock: true },
  { name: "book", price: 980, inStock: true },
  { name: "bag", price: 250, inStock: false },
];

// 命令型：合計用の変数を書き換えながら進める
let total = 0;
for (const item of items) {
  if (item.inStock) {
    total += Math.round(item.price * 1.1);
  }
}

// 関数型：絞り込み→変換→畳み込みを式でつなぐ
const total2 = items
  .filter((item) => item.inStock)
  .map((item) => Math.round(item.price * 1.1))
  .reduce((sum, p) => sum + p, 0);

console.log(total, total2); // 2398 2398
```

結果はどちらも2398です。命令型では`total`がループ中に何度も変わるので、値を追うには実行順をたどります。関数型の版は各段が「受け取った配列から新しい値を返す」だけなので、段ごとに切り出してテストできます。

Pythonでは同じ処理をジェネレーター式で書くのが標準的です。Python 3.14.6で実行すると同じ2398になります。

```
from functools import reduce

items = [
    {"name": "pen", "price": 1200, "in_stock": True},
    {"name": "book", "price": 980, "in_stock": True},
    {"name": "bag", "price": 250, "in_stock": False},
]

total = sum(round(i["price"] * 1.1) for i in items if i["in_stock"])

total2 = reduce(lambda s, p: s + p,
                map(lambda i: round(i["price"] * 1.1),
                    filter(lambda i: i["in_stock"], items)), 0)

print(total, total2)  # 2398 2398
```

なおPythonの`round`は偶数丸め（`round(2.5)`は2、`round(3.5)`は4）で、JavaScriptの`Math.round`とは端数の扱いが違います。

### 命令型・オブジェクト指向・関数型の比較

3つは併用でき、下の表は典型的な書き方の比較であって各方式の制約ではありません（オブジェクト指向でも不変オブジェクトやResult型は使えます）。違いは、状態をどこに置き、どう変えるかです。

| 観点     | 命令型       | オブジェクト指向         | 関数型                  |
| ------ | --------- | ---------------- | -------------------- |
| 処理の単位  | 文（手順）     | オブジェクトとメソッド      | 関数（式）                |
| 状態の扱い  | 変数を書き換える  | オブジェクト内に隠して書き換える | 新しい値を作って返す           |
| 繰り返し   | for・while | for・イテレーター       | map・filter・reduce・再帰 |
| 失敗の表し方 | 戻り値コード・例外 | 例外               | Result型・Option型などの値  |
| 再利用の手段 | サブルーチン    | 継承・委譲            | 高階関数・関数合成            |

オブジェクト指向の三本柱（カプセル化・継承・ポリモーフィズム）の実装は[オブジェクト指向とは？三本柱の実装手順とrecord・sealedまでJavaコードで解説](/tech/details/13412/)で扱っています。関数型は「状態を持つオブジェクト」を否定するのではなく、状態の変更を境界に寄せ、中心の計算を純粋関数で書く方向に働きます。

### ラムダ計算・LISP・Backus講演の系譜

理論の土台はAlonzo Churchのラムダ計算（1936年の論文「An Unsolvable Problem of Elementary Number Theory」）で、実装の起点はJohn McCarthyが1960年に発表したLISPの原論文です。命令型への対案として「関数型」を打ち出したのはJohn Backusの1977年のチューリング賞講演で、1989年にはJohn Hughesが「Why Functional Programming Matters」で高階関数と遅延評価がモジュール性を高めると論じました。

## 純粋関数・不変性・高階関数・関数合成の4つの中核

### 純粋関数と参照透過性

純粋関数は、(1)同じ引数なら必ず同じ値を返し、(2)関数の外にある状態を変えない関数です。外部変数の書き換え、ファイルやネットワークへの入出力、現在時刻や乱数の参照は、どれも副作用にあたります。

```
// 副作用あり：外の変数を書き換えるので、同じ引数でも結果が変わる
let total = 0;
function addToTotal(price: number): number {
  total += price;
  return total;
}

// 純粋関数：引数だけで結果が決まり、外の状態に触れない
function addTax(price: number, rate: number): number {
  return Math.round(price * (1 + rate));
}

console.log(addToTotal(100), addToTotal(100)); // 100 200
console.log(addTax(1000, 0.1), addTax(1000, 0.1)); // 1100 1100
```

`addToTotal(100)`は1回目に100、2回目に200を返します。呼び出しの順番や回数で結果が変わるため、テストでは事前に`total`を初期化する必要があります。`addTax`は式`addTax(1000, 0.1)`をいつでも値`1100`に置き換えられます。この性質を参照透過性と呼び、テストの書きやすさとキャッシュ（メモ化）のしやすさの根拠になります。

実務のプログラムから副作用は消せません。関数型の設計では、副作用をDBアクセスや画面描画などの外側の層に集め、判断や計算を行う内側を純粋関数にします。Reactのコンポーネントに求められる純粋性の条件は[Reactの純粋関数とは？3条件と「Cannot call impure function during render」の直し方](/tech/details/3772/)で解説しています。

### イミュータブルなデータとTypeScriptのreadonly

イミュータブル（不変）とは、一度作った値を書き換えず、変更が必要なときは新しい値を作ることです。TypeScriptでは`readonly`修飾子と`readonly string[]`（`ReadonlyArray`）で書き換えを型エラーにできます。

```
type Order = {
  readonly id: string;
  readonly items: readonly string[];
};

const order: Order = { id: "A-1", items: ["pen"] };
order.id = "A-2";
order.items.push("book");
console.log(order);
```

`tsc --strict`でコンパイルすると、次の2つのエラーが出ます。

```
b_readonly.ts(7,7): error TS2540: Cannot assign to 'id' because it is a read-only property.
b_readonly.ts(8,13): error TS2339: Property 'push' does not exist on type 'readonly string[]'.
```

ただし、tscはエラーがあってもJavaScriptを出力します（`noEmitOnError`の既定値は`false`）。出力されたJavaScriptをそのまま実行すると`{ id: 'A-2', items: [ 'pen', 'book' ] }`と表示され、書き換えは通りました。`readonly`は型検査のときにだけ働く約束で、実行時の保護ではありません。CIで`tsc --noEmit`を通すことを前提に使います。

実行時に書き換えを止める`Object.freeze`にも限界があります。

```
const order = Object.freeze({ id: "A-1", tags: ["new"] });
order.tags.push("sale"); // コンパイルも実行も通る（凍結は1階層だけ）
console.log(order.tags); // [ 'new', 'sale' ]

const scores = [30, 10, 20];
const sorted = scores.toSorted((a, b) => a - b); // 元配列を変えないES2023のメソッド
console.log(scores, sorted); // [ 30, 10, 20 ] [ 10, 20, 30 ]
```

`Object.freeze`が凍結するのは直下のプロパティだけなので、入れ子の`tags`には追加できました。配列は、ES2023で元の配列を変えない`toSorted`・`toReversed`・`toSpliced`・`with`が標準になっています。

言語レベルで深い不変データを持ち込む提案だったRecord & Tupleは2025年4月に取り下げられ、後継のComposites提案は[tc39/proposalsの一覧](https://github.com/tc39/proposals)でStage 2に置かれています（2026年11月4日に確認）。現時点のJavaScriptで深い不変性が必要なら、型の`readonly`に加え、Immer（npm最新11.2.9、2026年10月22日公開）のようにコピーを自動で作るライブラリを使います。

### 高階関数・カリー化・部分適用

高階関数は、関数を引数として受け取るか、関数を戻り値として返す関数です。最初の例の`map`・`filter`・`reduce`は、処理の中身を関数で受け取る高階関数です。関数を返す側の代表がカリー化で、複数の引数を取る関数を「1引数ずつ受け取る関数の連なり」に変換します。

```
// 関数を返す関数（カリー化）で、税率だけ先に固定する
const withTax = (rate: number) => (price: number): number =>
  Math.round(price * (1 + rate));

const jpTax = withTax(0.1);
console.log([100, 250, 980].map(jpTax)); // [ 110, 275, 1078 ]
```

`withTax(0.1)`は税率だけを固定した新しい関数を返します。一部の引数だけを先に与えて関数を作ることを部分適用と呼びます。できあがった`jpTax`は1引数の関数なので、そのまま`map`に渡せます。アロー関数とfunction宣言の違い、特に`this`の扱いは[アロー関数とは？functionとの違い・thisの挙動と使い分け【TypeScript・React・PHP】](/tech/details/3575/)を参照してください。

### 関数合成とpipe

関数合成は、ある関数の出力を次の関数の入力につないで、1つの関数にまとめることです。JavaScriptには合成の演算子がないため、左から順に関数を適用する`pipe`を自作するか、ライブラリのものを使います。

```
// 関数を左から順に適用する pipe
const pipe =
  <T>(...fns: Array<(x: T) => T>) =>
  (x: T): T =>
    fns.reduce((v, f) => f(v), x);

const trim = (s: string) => s.trim();
const lower = (s: string) => s.toLowerCase();
const normalizeEmail = pipe(trim, lower);
console.log(normalizeEmail("  Taro@Example.COM ")); // taro@example.com
```

この`pipe`は、すべての関数が同じ型`T`を受け取って返す場合だけを扱う簡略版です。段ごとに型が変わる合成を型安全に書く方法の1つが、引数の数ごとにオーバーロードを並べる形で、Effectの`pipe`は[effect](https://github.com/Effect-TS/effect) 4.1.2の型定義`Function.d.ts`で20個のオーバーロードを持ちます。

値を`|>`で次の関数へ流すPipeline Operatorも、tc39/proposalsの一覧でStage 2にとどまっています（直近の議論は2026年1月の会議）。標準化されるまでは、上のような関数で代用します。

## 関数型プログラミングのメリット・デメリットと向く処理

### メリットが大きい処理：変換・判定・並行処理

- **テストが入出力の比較で済む**：モックも事前の状態準備も要りません。料金計算のように分岐が多い業務ルールほど効果が大きくなります。
- **変更の影響範囲が戻り値に限られる**：関数を直したときに壊れうるのは、その戻り値を使う箇所だけです。
- **並行処理で競合が起きにくい**：書き換えられないデータは、複数のスレッドから同時に読んでもロックが要りません。Erlangの[並行プログラミングの解説](https://www.erlang.org/doc/system/conc%5Fprog.html)は、Erlangの実行単位がデータを共有しないことを「processes」と呼ぶ理由に挙げています。
- **画面の状態を計算として書ける**：[Reduxの用語集](https://redux.js.org/understanding/thinking-in-redux/glossary)は、reducerが純粋関数であることがホットリロードやタイムトラベルを可能にすると説明しています。

### デメリットと向かない処理：I/O中心・大量コピー・学習コスト

- **I/Oが中心の処理では恩恵が小さい**：ファイルを読んでDBへ書くだけのバッチでは、純粋関数に切り出せる部分がほとんど残りません。
- **新しい値を作り続けるコスト**：書き方によってはコピー量が件数の2乗で増えます（後半の失敗パターンの章で実測）。
- **再帰前提の書き方が動かない環境がある**：Node.js v26.5.0では末尾再帰の形でもスタックを消費し、1万段前後で尽きました（SafariのJavaScriptCoreは末尾呼び出しの最適化を実装済み）。
- **抽象の学習コスト**：モナドや型クラスに不慣れなメンバーが多いチームで多用すると、レビューと保守を担える人が限られます。

判断の基準は「関数の境界で純粋性を守り、内部は読みやすさを優先する」です。外から見て同じ入力に同じ出力を返し外の状態を変えなければ、内部でローカル変数を書き換えるループを使っても純粋関数として扱えます。

## 関数型プログラミング言語の一覧と主要言語のサポート状況

関数型言語は、副作用を型で区別する純粋関数型言語と、関数型を中心にしながら副作用も直接書ける言語に分かれます。純粋関数型言語のHaskellでは、ファイルの読み書きのような副作用を持つ処理は`IO`型の値として表され、型を見れば副作用の有無がわかります。

| 言語                  | 分類           | 実行環境・系統         | 特徴                |
| ------------------- | ------------ | --------------- | ----------------- |
| Haskell             | 純粋関数型        | GHC             | 遅延評価・型クラス・IOを型で分離 |
| Elm                 | 純粋関数型        | JavaScriptへ変換   | 主にWebフロントエンド向け    |
| Lean 4              | 純粋関数型        | ネイティブ           | 定理証明にも使う          |
| OCaml・F#            | ML系          | ネイティブ・.NET      | 型推論・パターンマッチ       |
| Clojure             | Lisp方言       | JVM             | 永続データ構造が標準        |
| Scala               | 関数型＋オブジェクト指向 | JVM             | Javaライブラリを直接使える   |
| Erlang・Elixir・Gleam | 関数型          | BEAM（Erlang VM） | 軽量プロセスで並行処理       |

個別の言語は、[Haskellの得意分野とは？何に使うか・将来性と仕事の実態【2026年】](/tech/details/5055/)、[Clojureとは？JVMで動くLisp系関数型言語の文法と環境構築・採用判断](/tech/details/7453/)、[Gleamとは？Erlang VMとJavaScriptで動く型安全な関数型言語【v1.19対応】](/tech/details/8943/)、[Lean4とは？入門・インストールからMathlib4・VSCode設定、最初の証明まで](/tech/details/8641/)で詳しく扱っています。副作用を型で追跡する仕組みをさらに一般化したものは[エフェクトシステムとは？Algebraic Effectsの仕組みと言語別の実装状況【2026年9月】](/tech/details/8714/)で解説しています。

### Java・C#・Python・C++・JavaScriptでの関数型機能

主要言語は、どれも関数型の機能を後から取り込んでいます。

| 言語         | 主な関数型機能                            | 導入時期                     |
| ---------- | ---------------------------------- | ------------------------ |
| Java       | ラムダ式・Stream API                    | JDK 8（2014年3月18日）        |
| C#         | LINQ・ラムダ式                          | C# 3.0（2007年11月）         |
| C++        | ラムダ式                               | C++11（2011年9月発行）         |
| Python     | functools・itertools・ジェネレーター        | 標準ライブラリ                  |
| JavaScript | toSorted・toReversed・toSpliced・with | ES2023（Node.js 20.0.0から） |

JavaのラムダとStream APIの使いどころは[Javaラムダ式の使いどころ｜Stream APIで効く場面と避ける場面をJava 25で実測](/tech/details/2598/)で扱っています。

### 関数型言語の利用率と主流にならない理由

[Stack Overflow Developer Survey 2025](https://survey.stackoverflow.co/2025/technology)の「過去1年に使った言語」（全回答者、回答31,771件）では、JavaScriptが66%、Pythonが57.9%、TypeScriptが43.6%です。関数型を中心にした言語は、Elixir 2.7%・Scala 2.6%・Lisp 2.4%・Erlang 1.5%・F# 1.3%・OCaml 1.2%・Gleam 1.1%で、どれも3%に届きません。HaskellとClojureは2025年版では選択肢に無く、自由記述（Write-Ins）で使ったと書いた回答はHaskellが0.05%、Clojureが0.03%でした。

利用率の差だけでは普及しない原因を特定できませんが、導入を判断する側から見ると、書き方の優劣より既存コードの移行とチームの学習にかかる費用のほうが重くなりがちです。利用経験の割合は採用できる技術者の数そのものではないため、人材の確保は採用する地域の市場で別に確かめます。言語別の人気の推移は[プログラミング言語の人気ランキングと推移【2026年10月最新】主要指標で読むトレンド](/tech/details/2418/)で比べています。

主要言語の側も、関数型の道具を全面的には採らず、選んで取り込んでいます。Python 3.0では`reduce`が組み込み関数から外され、[What’s New In Python 3.0](https://docs.python.org/3/whatsnew/3.0.html)には「Use functools.reduce() if you really need it; however, 99 percent of the time an explicit for loop is more readable.」と書かれています。一方で、純粋関数・不変データ・高階関数という考え方は、上の表のように主要言語の標準機能になりました。実務で関数型を学ぶ意味は、言語を移ることより、いま使っている言語での設計を変えることにあります。

## TypeScriptで書く関数型ドメインモデリング

関数型ドメインモデリングは、Scott Wlaschinの著書『Domain Modeling Made Functional』（Pragmatic Bookshelf、2018年1月）で広まった設計手法です。日本語版は『関数型ドメインモデリング』（猪股健太郎訳、KADOKAWA、2024年6月28日発売）です。原著の例はF#ですが、TypeScriptのユニオン型でも同じ設計ができます。要点は、業務上の状態ごとに別の型を作り、手続きを「ある型を受け取って次の型を返す関数」として書き、失敗を例外でなく値で返すことです。以下はEC注文の受付をこの考え方で書いた例です。

### 注文状態ごとの型分離

注文を「未検証」「検証済み」「価格計算済み」の3つの型に分け、`kind`で区別します。検証済みのメールアドレスは、ただの`string`と混ざらないようにブランド型`Email`にします。

```
type Email = string & { readonly brand: "Email" };
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

type UnvalidatedOrder = { kind: "unvalidated"; email: string; quantity: number };
type ValidatedOrder = { kind: "validated"; email: Email; quantity: number };
type PricedOrder = { kind: "priced"; email: Email; quantity: number; amount: number };
```

`kind`で型を見分ける書き方は判別可能なユニオン（Discriminated Union）と呼ばれます。`switch`での網羅チェックや`never`の使い方は[Discriminated Unionとは？判別可能なユニオン型の書き方とneverでの網羅チェック](/tech/details/6593/)で解説しています。

### Result型による注文受付ワークフローの合成

検証と価格計算をそれぞれ`Result`を返す関数にし、成功したときだけ次の関数へ渡す`andThen`でつなぎます。メールアドレスの検証は「@を含むか」だけの説明用の簡略版です。[RFC 5321の第2.4節](https://www.rfc-editor.org/rfc/rfc5321#section-2.4)はローカル部（@より前）を大文字と小文字を区別するものとして扱うよう定めているため、実務で全体を小文字化するかは受け付けるメールサーバーの仕様に合わせます。

```
const validate = (o: UnvalidatedOrder): Result<ValidatedOrder, string> => {
  const email = o.email.trim().toLowerCase();
  if (!email.includes("@")) return { ok: false, error: "メールアドレスが不正です" };
  if (!Number.isInteger(o.quantity) || o.quantity < 1 || o.quantity > 99)
    return { ok: false, error: "数量は1〜99で指定してください" };
  return { ok: true, value: { kind: "validated", email: email as Email, quantity: o.quantity } };
};

const price =
  (unitPrice: number) =>
  (o: ValidatedOrder): Result<PricedOrder, string> =>
    ({ ok: true, value: { ...o, kind: "priced", amount: unitPrice * o.quantity } });

// 成功時だけ次の関数へ渡し、失敗はそのまま素通りさせる
const andThen =
  <T, U, E>(f: (v: T) => Result<U, E>) =>
  (r: Result<T, E>): Result<U, E> =>
    r.ok ? f(r.value) : r;

const placeOrder = (o: UnvalidatedOrder) => andThen(price(1200))(validate(o));

console.log(placeOrder({ kind: "unvalidated", email: " Taro@Example.com", quantity: 3 }));
console.log(placeOrder({ kind: "unvalidated", email: "taro", quantity: 3 }));
console.log(placeOrder({ kind: "unvalidated", email: "taro@example.com", quantity: 0 }));
```

実行結果は次のとおりです。1件目は検証と価格計算を通って`amount: 3600`になり、2件目と3件目は`validate`の失敗がそのまま最後まで運ばれます。`try`・`catch`は1つもありません。

```
{
  ok: true,
  value: {
    kind: 'priced',
    email: 'taro@example.com',
    quantity: 3,
    amount: 3600
  }
}
{ ok: false, error: 'メールアドレスが不正です' }
{ ok: false, error: '数量は1〜99で指定してください' }
```

型を分けた効果は、誤った順序で関数を呼んだときに出ます。価格計算済みの注文だけを受け取る確認メール送信関数に、未検証の注文を渡すと型エラーになります。

```
const sendConfirmation = (o: PricedOrder): string => `${o.email} に ${o.amount}円の確認メールを送信`;
const draft: UnvalidatedOrder = { kind: "unvalidated", email: "taro@example.com", quantity: 1 };
sendConfirmation(draft); // 検証も価格計算も済んでいない注文は渡せない
```

```
e_domain.ts(35,18): error TS2741: Property 'amount' is missing in type 'UnvalidatedOrder' but required in type 'PricedOrder'.
```

「検証を飛ばしてメールを送る」という業務上の誤りが、テストを書く前に型エラーとして見つかります。成功と失敗の2本の線路でエラー処理をつなぐ手法は[鉄道指向プログラミング（ROP）とは｜Result型のbind・mapと5言語の実装、使わない場面](/tech/details/4705/)で、DDDの値オブジェクトや集約へ段階的に移す手順は[DDDの実装手順：値オブジェクトから集約まで既存コードを段階移行するTypeScript実例](/tech/details/17460/)で扱っています。

## 関数型で書いて遅くなる・落ちる失敗パターン

関数型の書き方は、JavaScriptの実行エンジンが得意な形と一致しないことがあります。次の2つが典型です。

### スプレッド構文で毎回コピーするreduce

配列からIDをキーにしたオブジェクトを作るとき、`reduce`の中で`{ ...acc, [key]: value }`と書くと、1件ごとにそれまでの全キーをコピーします。処理量は件数の2乗に比例して増えます。

```
const n = 4000; // 1,000・2,000・4,000で計測
const rows = Array.from({ length: n }, (_, i) => ({ id: "u" + i, v: i }));

// 遅い：1件ごとに累積オブジェクト全体をコピーする
const a = rows.reduce((acc, r) => ({ ...acc, [r.id]: r.v }), {});

// 速い：変換してから1回で組み立てる
const b = Object.fromEntries(rows.map((r) => [r.id, r.v]));
```

Node.js v26.5.0（macOS 26.6.2、x86\_64）で各件数を3回ずつ計測した結果です。絶対値はマシンの負荷に左右されますが、見るべきは、件数が2倍になるたびに時間が約4〜6倍に増えている点です。

| 件数     | スプレッドのreduce  | Object.fromEntries |
| ------ | ------------- | ------------------ |
| 1,000件 | 183〜255ms     | 0.4〜1.7ms          |
| 2,000件 | 737〜1,238ms   | 1.1〜2.4ms          |
| 4,000件 | 4,605〜5,474ms | 1.5〜2.5ms          |

Node.jsは1つのスレッドでJavaScriptを実行するため、4,000件で5秒前後かかるこの処理をリクエストの中で同期実行すると、その間はほかのリクエストにも応答できません。対処は、上の`Object.fromEntries`のようにコピーを伴わない組み立て方に変えるか、関数の内部だけでローカルな変数を書き換えるかです。不変性はデータを共有する境界で守れば足り、関数内部のループまで禁じる必要はありません。

### 再帰の深さとスタック上限

関数型言語はループの代わりに再帰を多用し、末尾位置の再帰呼び出しをループに変換する最適化を前提にしています。JavaScriptでは同じ書き方がそのまま動くとは限りません。

```
const sum = (xs: readonly number[], i = 0): number =>
  i === xs.length ? 0 : xs[i] + sum(xs, i + 1);

sum(Array(1000).fill(1));   // 1000
sum(Array(10000).fill(1));  // RangeError: Maximum call stack size exceeded
```

Node.js v26.5.0の既定設定では、カウンターを増やすだけの関数を自分自身で呼び続けると12,476段でスタックが尽きました（5回とも同値）。ECMAScript 2015の仕様には末尾呼び出しの最適化（proper tail calls）が含まれていますが、[compat-tableの対応表](https://compat-table.github.io/compat-table/es6/)で実装済みとされているのはSafari（10以降）だけです。累積値を引数で渡す末尾再帰の形に直した`sum`も、要素数100,000で同じ`RangeError`になりました。件数がデータ量で決まる処理は、再帰でなく`reduce`やループで書きます。再帰の仕組みと末尾再帰の書き方は[再帰関数とは？自分を呼ぶ関数の仕組みとスタック・末尾再帰・実務判断を解説](/tech/details/15264/)を参照してください。

### 関数型ライブラリをチームに入れる判断

TypeScriptで関数型の道具立てをそろえるライブラリには、次のものがあります（版と公開日は2026年11月4日にnpmレジストリの`time`フィールドで確認）。

| ライブラリ      | 最新版     | 公開日        | 範囲            |
| ---------- | ------- | ---------- | ------------- |
| neverthrow | 8.2.0   | 2025-02-21 | Result型だけ     |
| ts-pattern | 5.9.2   | 2026-10-14 | パターンマッチだけ     |
| fp-ts      | 2.16.11 | 2025-08-18 | 型クラス一式        |
| Effect     | 4.1.2   | 2026-11-02 | エラー・依存・並行処理まで |

Effectは2026年10月1日に4.0.0を公開しました。[fp-tsのREADME](https://github.com/gcanti/fp-ts)は、fp-tsがEffectのエコシステムに合流し、Effectをfp-ts v2の後継とみなせると説明しています。新規にfp-tsを選ぶ理由は薄くなりました。失敗を値で返したいだけなら、この記事の`Result`型のような20行程度の自作かneverthrowで足ります。Effectを入れるのは、レビュー担当の全員が`pipe`や`flatMap`の連鎖を読み、型エラーの原因を自分で特定できるようになってからにします。

## よくある質問

### 関数型プログラミングの例にはどのようなものがありますか？

コードでは、記事冒頭の`for`文を`map`・`filter`・`reduce`に置き換える書き方が代表例です。コード以外では表計算ソフトの数式が近く、セルの値から新しい値を計算し、元のセルを書き換えません。

### JavaScriptやTypeScriptは関数型言語ですか？

関数型専用の言語ではなく、命令型・オブジェクト指向・関数型を併用できるマルチパラダイム言語です。関数を値として変数に入れたり引数に渡したりできる（第一級関数）ため、高階関数や関数合成は自然に書けます。一方、変数の再代入やオブジェクトの書き換えは言語として禁止されていないので、不変性は`readonly`やレビューのルールで守ります。

### 関数型プログラミングとオブジェクト指向は併用できますか？

併用できます。よく使われる構成は、外部とのやり取りや状態の保持をクラスやモジュールに任せ、計算と業務判断を純粋関数で書く分担です。JavaやC#のrecordも値として扱うデータ型を簡潔に書く機能ですが、Javaのrecordが固定するのはフィールドの参照先までで、参照先のリストの中身は変えられます。C#のrecordは書き換え可能なプロパティも宣言できます。

### 関数型アーキテクチャとは何ですか？

判断や計算を行う中心部分を純粋関数で書き、DB・ネットワーク・画面への入出力を外側の薄い層に集める構成を指すことが多い言葉です。よく知られた呼び名は、Gary Bernhardtが2012年7月12日に公開したスクリーンキャスト[「Functional Core, Imperative Shell」](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell)です。中心部分は入出力なしでテストできます。

### 関数型プログラミングは何から学べばよいですか？

順番は、(1)普段使っている言語で`map`・`filter`・`reduce`を使い、ループを書き換える、(2)関数の中で外の変数を書き換えている箇所を探し、引数と戻り値だけで完結する形に直す、(3)失敗を例外でなく値で返すResult型を試す、が進めやすいです。設計まで学ぶなら『関数型ドメインモデリング』が、業務の状態を型で表す手順を具体的に扱っています。

## 関連記事

- [鉄道指向プログラミング（ROP）とは｜Result型のbind・mapと5言語の実装、使わない場面](/tech/details/4705/)
- [Discriminated Unionとは？判別可能なユニオン型の書き方とneverでの網羅チェック](/tech/details/6593/)
- [Reactの純粋関数とは？3条件と「Cannot call impure function during render」の直し方](/tech/details/3772/)
- [Haskellの得意分野とは？何に使うか・将来性と仕事の実態【2026年】](/tech/details/5055/)
- [再帰関数とは？自分を呼ぶ関数の仕組みとスタック・末尾再帰・実務判断を解説](/tech/details/15264/)

---

出典: [関数型プログラミングとは？純粋関数・不変性の基本とメリット・デメリットをTypeScriptの実例で解説](<https://www.issoh.co.jp/tech/details/4841/>)（株式会社一創）
