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

C# のジェネリクス経験者が TypeScript のジェネリクスで詰まる5つの違い(制約・型推論・共変性)

バイブス父さん
現役の業務SE
2026年7月16日11 min read
C# のジェネリクス経験者が TypeScript のジェネリクスで詰まる5つの違い(制約・型推論・共変性)

List<T>Dictionary<TKey, TValue> を息をするように使ってきた C# 出身者。その人が TypeScript でジェネリクスを書こうとすると、細かいところで手が止まる。

「あれ、List<string> って TS でどう書くんや? where T : の制約は? 型引数、書かなくてもいいん??」

どうも、ヒロポンです。

今回は C# のジェネリクス経験者が TypeScript のジェネリクスで詰まる5つの違いを、対応表とコードで整理します。C# で「型で縛る」感覚が体に入ってる人ほど、その資産はほぼそのまま効く。効くんだけど、5点だけ挙動がズレる。そこを先に潰しておくと、TS のジェネリクスで迷わなくなります。

俺も最初、List<T> のノリで List<string> と書いてエラー。「あ、そこからか」ってなりました。順番に見ていきます。

💡 C# 側の where T : 制約そのものを深掘りしたい人は C# Generic 制約 (where T : …) の5パターン にまとめてます。今回はその感覚を TS に翻訳するとどうズレるか の話。合わせて読むと橋渡しがスムーズです。

はじめに: C# の型の資産は効く。ただ5点だけズレる

C# のジェネリクスと TypeScript のジェネリクスは、書き方も考え方もよく似てます。<T> で型を受けて、制約で縛って、型安全に書く。この骨格は同じ。

土台がひとつ違う。TypeScript は 構造的型付け(structural typing) なんです。C# は「その型を継承してるか(名前で判定)」。TS は「その構造を満たすか(形で判定)」。この差が、ジェネリクスの制約や共変性の効き方にじわっと効いてきます。

今回挙げる5つは、その土台の違いから来る「C# の感覚だとハマるポイント」です。

対応マップ: C# ↔ TypeScript ジェネリクス

まず全体像から。よく使うやつの対応を、こんな感じでまとめます。

C# のジェネリクス機能 (List<T> / Dictionary / where 制約 / new() 制約 / default(T) / out T) を TypeScript にどう対応させるかの早見表

右の「注意点」列がこの記事の本体です。1つずつ見ていきます。

1. コレクション: List → Array / T[]、Dictionary → Map / Record

一番使うところから。List<T>Array<T>T[]。この2つは完全に同義です。

const names: Array<string> = ["山田", "佐藤"];
const nums: number[] = [1, 2, 3];   // Array<number> と同じ

Dictionary<TKey, TValue> は用途で2択。キーが文字列・数値で「決まったキーの集合」なら Record<K, V>。任意のキーで追加削除が多い汎用マップなら Map<K, V> です。

// キーが決まってる → Record
const scores: Record<string, number> = { math: 80, eng: 75 };

// 汎用マップ (追加削除が多い) → Map
const cache = new Map<string, number>();
cache.set("a", 1);

C# の Dictionary の感覚に一番近いのは Map の方。Record は「オブジェクトの型注釈」に近い使い方で、いい感じに JSON の型付けにハマります。

2. 型制約: where T : → extends(構造的型付けで効き方が違う)

C# の where T : SomeClass は、TS では <T extends SomeType>。ここまでは素直に対応します。

で、効き方が違う。C# の制約は「その型(クラス/インターフェース)を継承・実装してるか」で判定。TS の extends構造で判定します。

C# の where T : IEntity 制約と、TypeScript の T extends { id: number } 制約を並べ、TS は構造で判定されることを示す before/after

C# なら「IEntity を implements した型」に限定される。TS は id: number という形さえ満たせば何でも通る。わざわざインターフェースを implements しなくていい。緩いといえば緩いし、柔軟といえば柔軟。ここが構造的型付けの効きどころです!!

3. new() 制約 / default(T) は TS に無い(ファクトリで代用)

C# の where T : new()(引数なしコンストラクタを持つ制約)と default(T)。この2つは TS に直接の相当がありません

new() 制約でジェネリックにインスタンスを作りたい。そういう時は、コンストラクタ型を引数で受け取る

// C# の where T : new() で new T() する、に相当する書き方
function create<T>(ctor: new () => T): T {
    return new ctor();
}
class User { name = ""; }
const u = create(User);   // T は User に推論される

default(T) も無い。既定値が欲しいなら undefined を使うか、既定値を引数で受け取る形にします。ここは「C# のあの構文はそのまま来ない」と割り切って、書き方を変えるとこです。

4. 共変性: out T / in T は無い。配列は共変だが型安全でない

C# はインターフェースに out T(共変)/ in T(反変)を 宣言時に明示できます。TS には、この宣言時の共変性キーワードが(基本的に)ない。変性は構造から判定されます。

で、踏みやすいのがこれ。TS の配列は共変で、しかも型安全じゃない

class Animal { }
class Dog extends Animal { bark() {} }

const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs;   // 通る(配列は共変)

// でもこれがすり抜ける(実行時に壊れる余地)
animals.push(new Animal());       // Dog[] に Animal を入れてしまう

C# の配列も実は共変(で ArrayTypeMismatchException を投げる)。ただ TS は静的にここを止めてくれません。ん? Dog[]Animal 入れられたけど大丈夫なん??ってなるやつ。ジェネリックのコレクションを型引数違いで代入する時は、この共変の緩さを頭の隅に置いておくと事故りません。

5. 型引数は推論で消える( を明示しなくていい場面が多い)

C# だとジェネリックメソッドの型引数を明示することが多い。TS は 呼び出しの引数から型引数を推論します。だから <T> を書かなくていい場面がかなり多い。

function identity<T>(x: T): T {
    return x;
}

const a = identity(5);        // T は number に推論(identity<number> 不要)
const b = identity("hello");  // T は string に推論

C# の Identity<int>(5) の感覚で identity<number>(5) と毎回書くと、冗長になる。TS では推論に任せて <T> を消すのが普通。いい感じにスッキリ書けます。型が曖昧になる時だけ明示すればいい。最初はここ、律儀に書きすぎて「あれ、要らんのか??」ってなります。

(細かい話: const a = identity(5) だと厳密には数値リテラル型 5(number のサブタイプ)に推論されますが、5 は number として普通に使えるので、実用上は number と思って大丈夫です。)

まとめ / チートシート

C# のジェネリクスを TypeScript に持ち込む時の5点。

  • コレクション: List<T>Array<T> / T[]DictionaryMap / Record
  • 制約: where T :extends。ただし構造で判定(implements 不要で緩い)
  • new() / default(T): 相当なし。コンストラクタ型 new () => T / undefined で代用
  • 共変性: out T / in T の宣言はない。配列は共変だが型安全でないので注意
  • 型引数: 呼び出しから推論されるので <T> は消せる場面が多い

C# で培った「型で縛る」感覚は、TypeScript でもそのまま武器になります!! 土台が構造的型付けだと分かっていれば、この5つのズレはいい感じに全部腑に落ちる。ジェネリクスまで押さえておくと、モダンフロント寄りの案件でも C# の型の知識で戦える。あなたも一度は「TS は別世界に見える」って思ったこと、あるでしょ? その距離、思ってるより近いです。

よくある質問

Record と Map、結局どっちを使えばいい?

キーの集合が決まってて「オブジェクトの型」として扱うなら Record。キーが動的で追加削除が多いなら Map です。C# の Dictionary の使い方(実行時にキーを足したり消したり)に近いのは Map の方。JSON をそのまま型付けするような場面は Record が向きます。

extends が構造で判定されるって、型安全が緩くなるってこと?

「緩い」というより「別の基準」です。C# は名前(継承関係)、TS は構造(形)で判定する。形さえ合えば通るので implements 宣言が要らず柔軟。ただ、意図しない型がたまたま構造を満たして通ることもある。そこは id のような判別しやすいプロパティを制約に入れて絞るのが定石です。

共変性が無いと、C# の out T を使ってた設計はどう移す?

多くの場合、TS では変性を明示しなくても構造的に自然に代入互換になります。C# で IEnumerable<out T> の共変性に頼ってた読み取り専用の受け渡し。あれは TS では普通に上位型の配列やイテラブルとして受け取れることが多い。意図を明示したい時だけ、TS 4.7 以降の in / out 変性注釈で「検証」を付けられます(挙動は変えず、作者の意図チェック用)。

C# の generic メソッドと TS の generic 関数、書き味は同じ?

骨格は同じですが、TS の方が型推論が強くて <T> を省ける場面が多いです。関数型としての扱い(<T>(x: T) => T を型に書く)も含めて、delegate / Func の TypeScript 翻訳 系の記事と合わせて見ると、関数まわりの型の感覚が一通り埋まります。

次に読むべき記事

以上!

C# の型のノリで TS 書いて「あれ、違う」ってなった経験ある人、けっこう多いと思うんで、どんどんシェア待ってるぜ!!

執筆者

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

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


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

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

運営者について