TypeScript / Flow で学ぶ変性:共変・反変・不変・両変を簡単に理解する
みなさん、こんにちは!
しれっとオランダに移住したフロントエンドエンジニアの @nyaomaru です!🇳🇱
TypeScript を書いていると、“変性(variance)” という言葉をなんとなく目にすると思います。
- 共変(covariant)
- 反変(contravariant)
- 不変(invariant)
- 両変(bivariant)
このあたり、なんとなく分かったような、分からないような・・・?
僕自身もずっとふわっとした理解しかなく、特に 反変 と 両変 が直感に反していて苦しみました。
さらに、React の型定義を deep reading しようとした時に、
Flow のジェネリクス記号 +T / -T が意味不明で完全に詰みました。
export type Element<+C> = React$Element<C>;
export type RefSetter<-I> = React$RefSetter<I>;
「+ と - って何!?」「なんで React が Flow でこれを使ってるの?」
ここが 変性理解の入口 でした。
この記事では、TypeScript と Flow の両方を使いながら、
変性を “実務レベルで本当に使える形で” 簡単に理解することを目指します。
-
Reactの型を読めるようになりたい人 -
TypeScriptの安全なコールバック型を作りたい人 -
bivariantの罠を避けたい人
ほんなら、一緒に見てこな!
🧩 型の変性って何なん?
「型の変性」とは、総称型(ジェネリック型)や関数型で、
型パラメータのサブタイプ関係が、外側の型にどう伝わるか
を表す概念やな。
たとえば、Cat は Animal のサブタイプやけど、
- ほんなら
List<Cat>はList<Animal>として扱えるんか? -
Handler<Animal>を、Handler<Cat>の代わりに使えるんか? - あるいは、
Box<T>みたいな「中身が mutable な型」ではどうなん?
みたいな、「ジェネリック型のサブタイプ関係」を決めるためのルールや。
ほんで変性には以下の4つがある。
-
共変(covariant)
- 子 → 親 に広がる
-
反変(contravariant)
- 親 → 子 に縮む
-
不変(invariant)
- どちらの方向も不可
-
両変(bivariant)
- どちらの方向も OK(
TypeScriptの罠)
- どちらの方向も OK(
これらは TypeScript 固有のものやなくて、Java / C# / Kotlin / Flow みたいな、型のある言語ならどこでも登場する概念やねんな。
さて、これらの特性について個別に見ていこか〜
⬆️ 共変(covariant): 「子が OK なら親としても OK」
Cat が Animal のサブタイプ(Cat <: Animal)のとき、
共変:F<Cat> <: F<Animal>
つまり、「中身がもっと具体的なもの」を「もっと抽象的なもの」として扱えるパターン。
基本的には、読み取り専用の型として利用される。
class Animal {
name = 'animal';
}
class Cat extends Animal {
meow() {}
}
type ReadonlyBox<T> = {
readonly value: T;
};
const catBox: ReadonlyBox<Cat> = {
value: new Cat(),
};
// 「Cat の箱」を「Animal の箱」として扱うから OK
const animalBox: ReadonlyBox<Animal> = catBox;
// 読み取りは安全(Animal として読むだけ)
// animalBox.value.meow(); // これは型エラー(Animal には meow がない)
ここでは 「中身を 読む だけ」なので、より具体的な Cat を持つ箱を、
より抽象的な Animal の箱として見ることは(理論上は)安全、というのが共変やな。
- 上段:
Cat→Animal(サブタイプの依存関係) - 下段:
ReadonlyBox<Cat>→ReadonlyBox<Animal>(同じ向きに伝播 → 共変)
⬇️ 反変(contravariant): 「親が OK なら子専用にも使える」
Cat <: Animal のとき、
反変:
F<Animal><:F<Cat>
つまり、「より広いものを受け取れる関数」は「より狭いもの用としても使える」という発想。
ここで大事なのは、「反変」という概念は基本的に関数の引数で扱われるってことや。
そして、その型は書き込み専用で用いられる。
ここが直感と反してるから覚えにくいけど、「引数として扱われる値の話」と思うと理解しやすい。
class Animal {
name = 'animal';
}
class Cat extends Animal {
meow() {}
}
type Handler<T> = (value: T) => void;
const handleAnimal: Handler<Animal> = (animal: Animal) => {
console.log(animal.name);
};
const handleCat: Handler<Cat> = (cat: Cat) => {
cat.meow();
};
// 反変として安全なのはこっち
// 「Animal を受け取れる関数」を、「Cat 用のハンドラ」として使う
const catHandler: Handler<Cat> = handleAnimal; // OK(理論上)
// 逆方向は危険:Cat 専用ハンドラに Animal (Dog, Bird...) が来るかもしれない
// const animalHandler: Handler<Animal> = handleCat; // NG(理論上)
Handler<Animal> は「Animal ならなんでも受け取れる(Cat も含む)」から、
Handler<Cat> の代わりに使っても安全。
でも、逆は危険なのでダメ、というのが反変やな。
- 上段:
Cat→Animal - 下段:
Handler<Animal>→Handler<Cat>(逆向きに伝播 → 反変)
⛔ 不変(invariant):「どっちの方向もダメ」
Cat <: Animal でも、
不変:
F<Cat>とF<Animal>の間にサブタイプ関係は ない
class Animal {
name = 'animal';
}
class Cat extends Animal {
meow() {}
}
type Box<T> = {
value: T;
};
let animalBox: Box<Animal> = { value: new Animal() };
let catBox: Box<Cat> = { value: new Cat() };
// どっちの代入も型理論的には危険になる
// animalBox = catBox; // 本来は NG だが、TypeScript では共変寄りに扱われてコンパイルは通る
// catBox = animalBox; // NG (コンパイルエラー)
なぜか?
-
Box<Cat>をBox<Animal>として扱えるとしたら:-
Box<Animal>にnew Animal()を代入できてしまう - でも元は
Box<Cat>なので、「Cat しか入らない」と思って使ってるコードが破壊される
-
- 逆に
Box<Animal>をBox<Cat>として扱うのも同様に危険 - そのため型理論的には「どちらの代入も NG(不変)」にしたいが、
TypeScriptの実装上はBox<T>のような型は実質「共変寄り」に扱われており、Box<Cat>→Box<Animal>の片方向だけはコンパイルが通ってしまう
読みも書きもする mutable なコンテナは、不変にするのが安全、ってことやな。
- 上段:
Cat→Animal - 下段:
Box<Cat>とBox<Animal>の間には、サブタイプとしての関連がない → 不変
🔁 両変(bivariant):「どっち向きも OK(だから危険)」 ※ TypeScript の穴
Cat <: Animal のときに、
両変:
F<Cat>とF<Animal>が 相互に代入可能
つまり 共変 + 反変のミックス。
理論的にはほぼ確実に危険やけど、TypeScript は「現実の JavaScript コードとの互換性」のために、一部のケース(特に「コールバック引数」)でこれを許してる。
type EventHandler<T> = (event: T) => void;
declare let handleEvent: EventHandler<Event>;
declare let handleMouse: EventHandler<MouseEvent>;
// 本来の型理論的にはどちらかしか許されないはずだが、
// TypeScript では「両方向とも」代入できてしまうケースがある(bivariant)
handleEvent = handleMouse; // OK(実は危険になり得る。strictFunctionTypes: true で検知可能)
handleMouse = handleEvent; // これも OK
-
MouseEventはEventのサブタイプ - どちら向きでも代入 OK にすると、「ハンドラが期待していないイベント」が渡される可能性が出てくる
- でも
TypeScriptは「現実世界のコード量(DOM イベントハンドラなど)」を優先して、この緩さを残している
- 上段:
MouseEvent→Event - 下段:
Handler<MouseEvent>とHandler<Event>の間に両方向の矢印 → 両変
🎯 変性のまとめ
簡単にまとめると、以下のような表になるで〜
| 種類 | 説明 | 安全性 | 例 |
|---|---|---|---|
| 共変 | 子 → 親 OK | 読み取り専用 |
readonly T[], ReadonlyArray<T>
|
| 反変 | 親 → 子 OK | 書き込み専用 | Handler<T> |
| 不変 | 双方向 NG | ミュータブル | Box<T> |
| 両変 | 双方向 OK | 危険 |
TypeScript の関数引数 |
⚙️ TypeScript の特性
ここまで扱ったのは、あくまで「型理論としての綺麗な世界」。
- 読み取り専用なら共変
- 引数(書き込み専用)なら反変
- 読み書きするなら不変
- 両方向 OK は危険(両変)
って、非常に美しいルールやねんけど
TypeScript の現実は「ちょっと違う」
実は TypeScript は、
-
JavaScriptの歴史的仕様 - 既存の膨大なコード資産
- ブラウザ API の実装に合わせざるを得ない事情
などがあって、理論通りの変性をそのまま採用していないところが多いねんな。
典型的なのが次の 2 点:
- 配列(
T[])は本来「不変」やのに、ほぼ共変扱い - 関数の引数は本来「反変」やのに、両変が混ざっている
このあたりが 「型理論の感覚」と「現実の TypeScript の挙動」がズレて見える 主な理由やな。
ほんなら、この特有の変性の挙動も、一緒に見ていこか〜!
配列は本来「不変」であるべきやのに、TypeScript では“共変”として扱われる
まずは一番有名なズレ、配列(T[])の変性から見ていくで〜。
結論から言うと:
本来
T[]は「不変」にすべき
でもTypeScriptはほぼ共変として扱っている
その結果、理論上は「安全でない挙動」が普通に起きる
という状態になってもうてる。
例えば、以下のような感じやな。
class Animal {
name = 'animal';
}
class Cat extends Animal {
meow() {}
}
class Dog extends Animal {
bark() {}
}
const cats: Cat[] = [new Cat()];
// TypeScript 的には「Cat[] は Animal[] のサブタイプ」扱い
const animals: Animal[] = cats;
// Animal[] と見なされているから Dog を push できてしまう
animals.push(new Dog());
// でも実体は cat[]。中身が壊れてしまう。
const cat: Cat = cats[1]; // 型的には OK だが実体は Dog
cat.meow(); // 実行時エラーの可能性
なぜこれが起きるんか?
- 型理論的には
T[]は mutable container(読み書き可)- 本来は 不変 で扱うべき
- でも
JavaScriptの配列は API がめっちゃ広くて、歴史的にも「Array は自由に代入できるもの」として扱われてきた - その互換性を壊すと、既存コードが大量に壊れる
つまり、実用性を優先して安全性をしゃあないした結果、T[] が共変扱いになってる。
ほんで、代わりに TS が推奨してるのは「readonly」配列
const cats: readonly Cat[] = [new Cat()];
const animals: readonly Animal[] = cats; // こっちは理論的にも安全
readonly は読み取り専用やから、共変にしても壊れへんで〜。
関数の変性:本来は「反変」なのに、TypeScript はほぼ“両変”として扱われる
配列の次にズレが大きいのが 関数の引数の変性。
結論から言うと:
理論(型システムの世界):関数の引数は反変であるべき
TypeScript(現実):けっこうな範囲で両変として扱われる
この差が
「え、なんでこれ代入できるん?」
「なんで危険なコードのコンパイル通すの?」
という混乱ポイントになってる。
そもそも、関数はなんで“反変”なのか?
type Handler<T> = (value: T) => void;
この Handler<T> の「T の位置」は
- 値を受け取る側(=書き込み) なので
- 本来は 反変 になる
つまり理論的にはこう:
Cat <: Animal なら
Handler<Animal> <: Handler<Cat>
前の章でやったやつやな。
実際の TypeScript:なぜか両方向に代入できる
大量の JavaScript 資産との互換のため、
「関数の引数」に関してだけ、特別に両変として扱う部分がある
(strictFunctionTypes: falseやメソッドの引数など、一部のケースでは)
type Handler<T> = (value: T) => void;
declare let handleEvent: Handler<Event>;
declare let handleMouse: Handler<MouseEvent>;
// 本来の型理論的にはどちらかしか許されないはずだが、
// TypeScript では「両方向とも」代入できてしまうケースがある(bivariant)
handleEvent = handleMouse; // OK(危険になり得る)
handleMouse = handleEvent; // OK(これも危険)
理論では絶対 NG なのに、TypeScript は両方向 OK にしてもうてる。
ほんで、以下のパターンはかなり危険な例やな。
type Handler<T> = (value: T) => void;
const handleEvent: Handler<Event> = (event) => {
console.log(event.type);
};
const handleMouse: Handler<MouseEvent> = (event) => {
// MouseEvent にしかないプロパティ
console.log(event.clientX);
};
// 「MouseEvent 専用の handler」を「Event handler」に代入できてしまう
const eventHandler: Handler<Event> = handleMouse;
// Event が来るかもしれないのに、MouseEvent として扱ってしまう
eventHandler(new Event('click')); // ← 型は OK だが実行時エラー
これは 「本来絶対にコンパイルエラーにすべきパターン」やけど、両変のせいで通してまう。
なんで TypeScript はこんな危険な仕様になったん?
理由はめちゃくちゃ単純で:
現実の JavaScript が「厳密な反変」を前提として作られてないから。
-
DOMイベントハンドラの仕様 -
Node.jsのコールバック慣習 -
JavaScriptで書かれた膨大なコード
これらが「ゆるい関数代入」を前提にしてたので、
TypeScriptを理論通りにすると、既存コードが破壊されまくる
工数と影響がでかすぎて無理 → しゃあない
っていう、安全性(soundness)より実用性(pragmatism)を優先したっちゅう至極真っ当な考えによるんやな。
じゃあ“厳密な反変”で関数を扱いたい時は?
実は TypeScript には裏オプションがあって、
--strictFunctionTypes をオンにすると
コールバックの引数だけ 反変寄り のチェックになる
{
"compilerOptions": {
"strict": true,
"strictFunctionTypes": true
}
}
ただし、「完全に反変」になるわけではなくて、
普通の関数型の引数は反変寄りになる一方で、メソッドの引数は依然として両変のまま
という、びみょ〜な仕様になってる点は注意やで〜。
🎯 まとめ:TypeScript は「変性」を完璧に守る世界線やない
ここまで見たように、
- 配列(
T[])は、本来は不変 → ほぼ共変 - 関数の引数は、本来は反変 → 広い範囲で両変
-
strictFunctionTypesを ON にしても完全に反変にはならない
つまり TypeScript は、
型理論の「綺麗な世界」を完全には採用せず、
JavaScriptの現実と巨大コード資産を守る設計になっている
というわけやな。
🌊 Flow では、変性がどのように表現されているのか?
Flow とは?
ここで一旦、冒頭でも出てきた Flow(FlowType) を軽くおさらいしとこか。
Flow は Facebook(現 Meta)が開発した JavaScript の静的型チェッカーで、TypeScript と同じ「型を付ける」系のツールやねん。
特徴はこんな感じ:
- 型チェックは
TypeScriptより厳密 - 変性(variance)を明示的に扱う
- 型推論がかなり強い
-
JavaScriptの構文そのままに注釈を付ける
一言でいうと、
TypeScriptは「実用性重視」
Flowは「安全性重視」
その安全性の象徴が、今回のテーマである 変性(variance)を明示する機能 なんや。
Flow では変性をどうやって書くん?
TypeScript と違って、Flow ではジェネリクスの型パラメータに「変性記号」を明示的に書けるようになっている(何も付けない場合は不変扱い)。
Flow の変性記号はこれや 👇
| Flow の記号 | 意味 |
|---|---|
+T |
共変(covariant) |
-T |
反変(contravariant) |
T(記号なし) |
不変(invariant) |
つまり Flow では、「この型はどう使われるか?」を開発者が 明確に宣言する。
TypeScript では暗黙的に推論でやってくれるけど、Flow は安全性のために明示するスタイルを取ってるわけやな。
⚛️ React での宣言はどうなってるん?
Flow が React 開発元(Meta)製ということもあって、React 本体の型定義には長らく Flow が使われてきてんな。
今もリポジトリには Flow 由来の型定義が残っていて、その名残がこれ 👇
export type Element<+C> = React$Element<C>;
export type RefSetter<-I> = React$RefSetter<I>;
-
+Cは 共変 -
-Iは 反変
を意味してるねん。
なんで Element の型パラメータは共変なん?
理由はシンプルで:
ReactElement は immutable(不変)な構造やし、
「要素を強い型に変換すること」はあっても、「書き込む」ことはせえへん。
だから Flow 的には +C(共変)で宣言されとるわけやな。
例えば
type AnimalProps = { name: string };
type CatProps = { name: string; meow: () => void };
function Cat(props: CatProps) {
return null as any;
}
const catElement: ReactElement<CatProps> = (
<Cat name="nyaomaru" meow={() => {}} />
);
// 共変なので AnimalProps として扱っても安全
const parent: ReactElement<AnimalProps> = catElement;
- 「より細かい型の props を持つ要素」 を
- 「より抽象的な props の型として扱える」
これはめちゃくちゃ便利やし妥当。
React はこの「共変性」をうまく利用しているわけやな。
なんで RefSetter の型パラメータは反変なん?
理由は:
-
RefSetterは 「値(インスタンス)を受け取る側」 - 書き込み方向だけが発生する
- つまり、引数位置の変性は反変になる
これは前の Handler の話とも完全に対応してるな。
例えば
type HTMLDivRef = (el: HTMLDivElement | null) => void;
type HTMLElementRef = (el: HTMLElement | null) => void;
HTMLDivElement <: HTMLElement なので、反変で考えると:
RefSetter<HTMLElement> <: RefSetter<HTMLDivElement>
つまり、
- HTML 要素ならなんでも受け取れる関数(広い受け口)
- が、特定の
refの代わりとして安全に使える
これは正しい反変の使い方やな。
逆向きは絶対危ない:
- 「div しか受け取れません!」という関数に
- 「もっと広い
HTMLElement」が入ってくる可能性
つまり、実行時エラーの温床。
やから -T になってるわけやな。
Flow の変性を押さえると React の型が急に読みやすくなる
ここまでで分かるように、React の型には
-
Element系 → 共変(+T) -
RefSetter→ 反変(-T) - Mutable なもの → 不変
という「安全な変性設計」 が一貫して適用されてる。
Flow の変性記法を知ってると、
「
Reactの型がなぜこう設計されてるか」が一気に見えるようになる
んよな。やから、変性の概念を理解しておくと、React のコードが一気に読みやすくなるで〜
Flow の変性のまとめ
-
FlowはFacebook製のJavaScript型チェッカー(Reactと親和性が高い) -
Flowの特徴は 変性の明示(+T: 共変 /-T: 反変 /T: 不変)-
ReactElement<+C>は読み取り専用 → 共変(+C) -
RefSetter<-T>は値を受け取るだけ → 反変(-T) - 一般的な
Props<T>のように、読み書きの両方が起きる可能性がある型 → 不変(T)
-
-
FlowのおかげでReactの型は一貫して安全に設計されていた
🛠️ 実務でどこに効いてくるの?
ここまでの話は一見「理論」っぽいけど、実務でもよく顔を出すで〜
-
onClick,onChange,onSubmitみたいな UI イベントハンドラ -
onSuccess,onErrorみたいな API コールバック
これらは全部「引数を受け取る関数」なので、本来は 反変 で考えるべきもの。
たとえば公開 API の型を設計するときは、
- コールバックの引数は できるだけ広い型 を受け取れるようにしておく
- 逆に「特定のサブタイプしか想定してないハンドラ」を、汎用ハンドラの代わりに雑に代入しない
みたいな感覚があると、両変の罠を踏みにくくなるんちゃうかな。
readonly を付けるかどうか、Box<T> を公開するか ReadonlyBox<T> だけにするか、
こういう API 設計も、実は全部「変性の話」やったりするので、頭の片隅に置いておくとだいぶ設計が楽になるはずや!
📌 まとめ
ここまで見てきたように、変性そのものは日常で名前を意識することは少ないけど、
-
readonlyにするかどうか - コールバックの引数の型をどこまで広くするか
-
Box<T>みたいなコンテナをそのまま公開するか、ReadonlyBox<T>だけにするか -
Reactの型定義を読んだときに「なんでこう書いてあるのか」を納得できるかどうか
みたいなところで、じわじわ効いてくる概念やと思う。
「共変・反変・不変・両変」を、暗記するものというより「API 設計のときに思い出すチェックリスト」くらいに捉えてもらえるとちょうどええんかな〜と。
あと、今回の変性の型を検証できるリポジトリを作ったから、気になったら遊んでみてな!👇
ほな、ええ変性ライフを 🐈⬛✨
参考にしました
おまけの宣伝
TypeScript を書いていると、isXXXってちょこちょこ出てこうへん?
あれ毎回宣言するのめんどくさいし、結構手間。
せやから簡単に構築できる is-kit っちゅう OSS 作ってみたから、遊んでいってなぁ〜!🐈

関連記事もチェックしてなぁ〜!😸
Discussion