動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# の delegate / Action / Func を TypeScript の関数型に翻訳する(型推論の違いで詰まらない)

バイブス父さん
現役の業務SE
2026年7月13日13 min read
C# の delegate / Action / Func を TypeScript の関数型に翻訳する(型推論の違いで詰まらない)

「来月から TypeScript のプロジェクト手伝ってもらうかも」

同期にそう言われて、家で軽く触ってみた時の話です。C# の Func<int, int> のノリで関数を変数に突っ込もうとしたら、いきなり手が止まった。

型、どう書くんや??

どうも、ヒロポンです。

今回やるのは、C# の delegate / Action / Func を TypeScript の関数型に翻訳する話。Func と TypeScript の関数型は、ぱっと見ほぼ同じに見えます。ただ引数の型推論、戻り値 void の扱い、ジェネリック関数のあたりで挙動が違う。そこを対応表で押さえておくと、C# の感覚のまま TypeScript が書けます。

C# を10年触ってきた人ほど、「関数を型として扱う」感覚は体に入ってるはず。その資産、ほぼそのまま使えます。差分だけ埋めていきましょう。

俺の体験: C# の Func のノリで書いて詰まった

最初に踏んだのは、拍子抜けするくらい単純なやつでした。

C# ならこう書く。

Func<int, int> f = x => x * 2;

これを TypeScript で const f = x => x * 2; と写したら、エディタが x に赤線を引いてきた。「Parameter 'x' implicitly has an 'any' type」。

C# だと Func<int, int> という型が先にあるから、x は int だと推論される。ところが TypeScript で型注釈なしにいきなりアロー関数を書くと、x の型を推論する手がかりが無くて any に落ちる。

この「似てるけど手がかりの与え方が違う」を最初に整理しておくと、後がだいぶラクになります。

対応マップ: C# の delegate / Action / Func ↔ TypeScript の関数型

まず全体像から。よく使うやつを対応表にすると、こんな感じで写せます。

C# の delegate / Func / Action / Predicate / ジェネリックメソッドを、TypeScript の関数型にどう対応させるかの早見表

Func の最後の型引数が戻り値、残りが引数。TypeScript は素直に (引数: 型) => 戻り値 と書くだけ。ここまでは C# の知識がそのまま効くんで、いい感じにスタートを切れます。

問題は、この表の右側を実際に書き始めた時。「推論と void」の差がじわっと出てきます。順に見ていきましょう。

対応1: Func<T, U> ↔ (x: T) => U(型注釈の置き場所が違う)

Func<int, int> を変数に入れるなら、TypeScript では左辺に型を書きます。

// C# の Func<int, int> f = x => x * 2; に相当
const f: (x: number) => number = x => x * 2;

左辺に (x: number) => number を付けると、右辺のアロー関数の x は「文脈から」number だと推論される。これをcontextual typing(文脈的型付け)と言います。C# の delegate 型が推論の手がかりになるのと、役割は全く同じ。

引数側に直接型を書くやり方でもいい。

const f = (x: number): number => x * 2;   // これでもOK

どっちでも動きます。手がかりがどこか1箇所にあればいい、これが C# との一番の違い。C# は delegate 型が必須。TypeScript は「左辺の型」か「引数の型注釈」、どちらかがあれば推論が回ります。

対応2: Action ↔ (x: T) => void(void の扱いが C# より緩い)

Action<int> は「int を受け取って何も返さない」。TypeScript なら (x: number) => void です。

const log: (msg: string) => void = msg => console.log(msg);

ここまではいい。厄介なのは、TypeScript の void 戻り型が C# の Action より緩いという点。

() => void を要求してる場所に、値を返す関数を渡してもエラーにならない。戻り値がただ無視されるだけなんです。

const nums: number[] = [];
// push は number を返すが、forEach のコールバックは (x) => void を期待
// なのに通る(戻り値が捨てられる)
[1, 2, 3].forEach(x => nums.push(x));

C# の Action<int> に「int を返すメソッド」を代入したらコンパイルエラー。TypeScript はこれを通す。forEachpush を直接渡せるようにするための、意図的な緩さです。

便利な反面、「void のつもりが値を返してた」を見逃しやすい。ここは後のハマりポイントでもう一度触ります。

対応3: delegate 宣言 ↔ type 別名 / ジェネリック関数

C# で名前付きの delegate 型を宣言するやつ。TypeScript では type 別名に写します。

C# の delegate 宣言と Func を、TypeScript の type 別名による関数型に翻訳する before/after

ジェネリックは、似て非なるやつ。C# のジェネリックメソッド T Identity<T>(T x) は、TypeScript ではこう書きます。

const identity = <T,>(x: T): T => x;

ここで引っかかるポイントが2つあります。

ひとつ。.tsx(React)ファイルだと <T> が JSX タグと区別できず構文エラーになる。回避策は <T,> とカンマを足すか、<T extends unknown> と書いて逃がす。.ts ファイルなら <T> だけでいけます。

もうひとつは、型引数を「どこで縛るか」。C# のジェネリックメソッドは呼び出しごとに T が決まる。TypeScript も同じことができるけど、type 別名で書く時は置き場所で意味が変わってきます。

// A: 呼び出しごとに T が決まる(C# のジェネリックメソッドに近い)
type Identity = <T>(x: T) => T;

// B: 型を使う側で T を決める
type IdentityOf<T> = (x: T) => T;

A は <T> が関数型の中にあるので、実際に呼ぶ瞬間に T が決まる。B は IdentityOf<string> のように、型を使う場所で T を固定する。C# の感覚に置き換えると、A が「ジェネリックメソッド」、B が「型引数を持つ delegate」に近い。ここは最初、頭がこんがらがるとこです。

ミニマム検証のハンズオン

手元で確かめるなら、tsc(TypeScript コンパイラ)で型チェックだけ回すのが早いです。

# 型チェックだけ(JS は出力しない)
npx tsc --noEmit --strict sample.ts

--strict を付けると、暗黙 any(対応1で出たやつ)がちゃんとエラーになる。逆に付けないと素通りするんで、C# の感覚で「型が緩い言語だな」と勘違いしがち。TypeScript は strict で書いてこそ C# に近い安心感が出ます!! いい感じに縛って使いましょう。

ハマりポイント: C# の感覚とズレるところ

ハマり1: 型注釈なしのアロー関数は引数が any になる

冒頭で詰まったやつ。const f = x => x * 2;x が推論できず、strict なら「implicitly has an 'any' type」でエラー。

C# は delegate 型が常に手前にあるから、こんな迷い方はしない。TypeScript は左辺の型か引数の型注釈、どちらかを必ず添える。関数をコールバックとして渡す時(arr.map(x => ...))は、map 側が型を持ってるから注釈なしでも効きます。単独で変数に入れる時だけ気をつければいい。

ハマり2: void 戻り型は「値を返しても代入互換」

対応2の緩さ。これが地味に効いてきます。

type Handler = () => void;
const h: Handler = () => 42;   // エラーにならない(42 は捨てられる)

() => void を要求してる型に、値を返す関数を入れても通る。C# の Action 感覚だと「え、戻り値あるやんか??」ってなるとこですが、TypeScript はこれを許す。

実害が出るのは、コールバックの戻り値を後で使おうとした時。「void 型だから返ってこない」前提で設計すると噛み合わない。戻り値を使いたいなら型を () => number みたいに明示する。voidに逃がさない。

ちなみに緩いのは「代入先の型が () => void」の時だけ。関数側で (): void => 42 と自分で戻り型に void を書いて値を返すと、こっちは TS2322(number は void に代入できない)でちゃんと怒られます。境目はここ。

ハマり3: .tsx でジェネリックアロー関数が JSX と衝突

React を触る現場だと踏むやつ。.tsxconst f = <T>(x: T) => x; と書くと、<T> が JSX タグの開始と解釈されて構文エラー。

対策は <T,> とカンマを付けるか、<T extends unknown> と書く。.ts なら起きないんで、「TypeScript の入門記事のコードをそのまま .tsx に貼ったら動かん」の原因はだいたいこれ。ん?さっきのサンプル動いたのに??ってなったら、拡張子を疑ってください。

俺の現場メモ: 型で縛る感覚がそのまま資産になる

C# を長くやってると、「この関数は int を受けて int を返す」を型で縛る感覚が体に染み込んでる。これ、TypeScript でめちゃくちゃ効く資産なんですよね。

TypeScript 入門でつまずく人の多くは、そもそも「関数を型で表す」発想が無いところから入る。C# 出身者はそこを丸ごと飛ばして、Func を関数型に読み替えるだけでいい。差分は今回の「推論の手がかり」と「void の緩さ」くらいのもんです。

俺が実際に移った時も、詰まったのは言語仕様の細部じゃなかった。この2つの「似てるけど微妙に違う」ところだけ。裏を返すと、そこさえ押さえれば C# の型の知識がそのまま武器になります!! 変に身構えなくていい。良い感じに地続きなんですよね。

まとめ

C# の関数型を TypeScript に翻訳するポイント。

  • Func<T, U>(x: T) => UAction<T>(x: T) => voiddelegate 宣言 → type 別名
  • 型注釈なしのアロー関数は引数が any になる。左辺か引数のどちらかに型を添える
  • () => void は値を返す関数も受け付ける(C# の Action より緩い)。戻り値を使うなら型を明示
  • ジェネリック関数は .tsx<T,> が必要。型引数を「どこで縛るか」で意味が変わる

C# で培った「型で縛る」感覚は、TypeScript でほぼそのまま通用します。差分だけ埋めれば、モダンフロント寄りの案件にも手が届く。TypeScript を1個押さえておくと、次のキャリアの選択肢がちょっと広がるんですよね。

よくある質問

Func と TypeScript の関数型、完全に同じと思っていい?

書き方はほぼ同じですが、2点だけ違います。引数の型推論に「手がかり」が要ること(型注釈なしだと any)と、void 戻り型が緩いこと。この2つを意識しておけば、あとは C# の感覚で書けます。

delegate の multicast(+= で複数登録)は TypeScript にある?

言語機能としては無いです。C# のマルチキャストデリゲートに相当するものは、TypeScript では「関数の配列」を自分で持って順に呼ぶ、という形で書きます。イベント的な仕組みが欲しいなら、コールバック配列や既存のイベントライブラリを使うのが素直です。

型注釈はどこまで書くべき? 全部書くと C# より冗長では?

コールバックで渡す時(arr.map(x => ...))は、渡し先が型を持ってるので注釈は要りません。単独で変数に関数を入れる時だけ左辺に型を書く、が目安です。全部書く必要はなくて、推論が効かない場所だけ補う感覚が近い。

strict は必須? とりあえず動かすだけなら要らない?

C# 出身者ほど --strict を付けた方がいいです。暗黙 any や null チェックが有効になって、C# に近い「型で守られてる」感覚になる。strict なしは型の緩い別言語だと思った方がよくて、せっかくの型の資産が活きません。

次に読むべき記事

以上!

C# からモダンフロントに橋渡ししてる仲間、けっこう増えてると思うんで、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年(正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SIer 正社員から独立し、複数のフリーランスエージェント経由で現場を渡り歩いた経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について