「来月から 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 の関数型
まず全体像から。よく使うやつを対応表にすると、こんな感じで写せます。

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 はこれを通す。forEach に push を直接渡せるようにするための、意図的な緩さです。
便利な反面、「void のつもりが値を返してた」を見逃しやすい。ここは後のハマりポイントでもう一度触ります。
対応3: delegate 宣言 ↔ type 別名 / ジェネリック関数
C# で名前付きの delegate 型を宣言するやつ。TypeScript では type 別名に写します。

ジェネリックは、似て非なるやつ。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 を触る現場だと踏むやつ。.tsx で const 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) => U、Action<T>→(x: T) => void、delegate宣言 →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 で経歴詳細を見る


