【入門編】TypeScriptの「構造的部分型」を理解する:Interfaceの互換性チェックの裏側 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。
普段、JavaやC#、C++といった「名前的型付け(Nominal Typing)」が主流の言語を使っていると、TypeScriptのコードを書いたときに「えっ、これでエラーにならないの!?」とか、逆に「なんでここで代入できないんだ……?」と戸惑う瞬間が必ずやってきますよね。

ここをクリアするかどうかで、TypeScriptがただの「めんどくさいJavaScriptのラッパー」に見えるか、それとも「最高の開発体験をもたらす最強の相棒」に見えるかが決まります。

今回は、TypeScriptの型システムの心臓部である「構造的部分型(Structural Subtyping)」の裏側を、優しく、そして本質的に解き明かしていきますね。ここさえ押さえれば、TypeScriptの型はもう怖くありません。一緒にバッチリマスターしていきましょう!

—

1. そもそも「名前的型付け」と「構造的部分型」って何が違うの?

他の言語から来たエンジニアが一番最初に混乱するのがここです。

  • 名前的型付け(Nominal Typing)
  • JavaやC#などの世界です。
  • 「人間にとっての名前」がすべて。たとえ中身のデータ構造が全く同じであっても、クラス名やインターフェース名が違えば、それは全く別のものとして扱われます。
  • 構造的部分型(Structural Subtyping)
  • TypeScriptの世界です(いわゆるダックタイピングの静的型付け版)。
  • 「もしそれがアバターのように歩き、アバターのようにガーガー鳴くなら、それはアバターである」という考え方。つまり、「必要なプロパティ(構造)さえ持っていれば、名前が違っても同じとみなす」というルールです。

言葉だけだと抽象的なので、コードでその違いを体感してみましょう。

// ユーザーを表すインターフェース
interface User {
name: string;
age: number;
}

// ゲストを表すインターフェース(中身はUserと全く同じ)
interface Guest {
name: string;
age: number;
}

const tanaka: User = { name: “田中”, age: 25 };

// さあ、ここで問題です!
// 構造的部分型の世界では、この代入は成功するでしょうか?
const guestUser: Guest = tanaka;

JavaやC#の感覚だと、「`User`型を`Guest`型に代入するなんて、名前が違うからコンパイルエラーになるはず!」と思いますよね。

でも、TypeScriptのコンパイラはこう考えます。
> 「おっ、`Guest`型が求めているのは `name: string` と `age: number` だね。で、渡された `tanaka` はその両方を持っている。構造的に満たされているから、OKです!」

そのため、上記のコードはエラーなくスッと通ります。これが構造的部分型の魔法です。

—

2. 構造的部分型の「包容力」と、ちょっとした罠

TypeScriptのこの構造的部分型、コードをシンプルに書く上でめちゃくちゃ強力な包容力を持っています。例えば、関数の引数に渡すオブジェクトを考えてみましょう。

interface Point2D {
x: number;
y: number;
}

function printPoint(p: Point2D) {
echo(`X: ${p.x}, Y: ${p.y}`);
}

// 1. 普通にインターフェースを満たすオブジェクトを渡す
printPoint({ x: 10, y: 20 }); // OK

// 2. なんと、余計なプロパティが含まれていてもOK!
const point3D = { x: 10, y: 20, z: 30 };
printPoint(point3D); // これもエラーになりません!

`printPoint` 関数は `x` と `y` さえあれば満足なので、`z` が余分にくっついていても「お、必要最低限のパーツは揃ってるね!」と受け入れてくれます。これが実務ですごくラクに働くんです。

ただし、直接オブジェクトリテラルを渡すときの「厳格なチェック」に注意!

ここで、初心者が一番ハマりがちな罠を紹介します。先ほどの `point3D` を変数に入れずに、直接関数の引数に書いてみると……?

// 直接オブジェクトリテラルを渡してみる
printPoint({ x: 10, y: 20, z: 30 });
// ❌ ここでコンパイルエラー!
// 「型 ‘{ x: number; y: number; z: number; }’ の引数を型 ‘Point2D’ のパラメータに割り当てることができません。オブジェクト リテラルは既知のプロパティのみを指定できますが、’z’ は型 ‘Point2D’ に存在しません。」

「あれ?さっき変数に入れたときは通ったのに、なんで直書きすると怒られるの?」って思いますよね。

これはTypeScriptの「オブジェクトリテラルに対する過剰プロパティチェック(Excess Property Checking)」という特別なルールです。
変数経由のときは「構造を満たしているか」だけで判断しますが、関数の引数などに直接オブジェクトをドンと置いたときは、「タイポ(打ち間違い)をしていないか?」をコンパイラが親切心(おせっかい?)で厳しくチェックします。

「あ、プロパティ名 `z` って打っちゃったけど、もしかして `x` や `y` のつづりミスじゃない?」とTypeScriptが心配してエラーにしてくれるんですね。

—

3. インターフェースの互換性を自分でコントロールする

「構造が一緒なら何でも代入できちゃうのは、逆に困ることもあるんだけど……」
そんなときは、TypeScriptの型システムにちょっとした「目印」をつけて、名前的型付けのような挙動を再現することができます。

実務でよく使われるテクニックが「ブランド型(Branded Types)」です。

// ユーザーIDと商品ID、どちらも実体はただの「文字列(string)」
type UserId = string & { readonly __brand: unique symbol };
type ProductId = string & { readonly __brand: unique symbol };

// ヘルパー関数で安全に型を生成する
function createUserId(id: string): UserId {
return id as UserId;
}

function createProductId(id: string): ProductId {
return id as ProductId;
}

const userId = createUserId(“user_123”);
const productId = createProductId(“prod_456”);

function printUser(id: UserId) {
console.log(`User ID: ${id}`);
}

// 正しいIDを渡すのはもちろんOK
printUser(userId);

// ❌ うっかり商品IDを渡してしまうと……?
printUser(productId);
// コンパイルエラー!
// 「型 ‘ProductId’ の引数を型 ‘UserId’ のパラメータに割り当てることができません。」

中身はどちらも `string` なのに、見えないおまけのプロパティ(`__brand`)を構造に混ぜ込むことで、TypeScriptに「この2つは別物として扱って!」と指示できるわけです。
構造的部分型のベースを持ちながら、必要な場所だけ厳格に型を区別する。これが使いこなせると、シニアエンジニアの仲間入りです!

—

まとめ:TypeScriptの型は「書類の審査員」

ここまでのポイントをギュッとまとめてみましょう。

1. 構造的部分型とは、「名前」ではなく「中身(構造)」で互換性を判断する仕組み。
2. 必要なプロパティさえ持っていれば、別のインターフェースや追加のプロパティがあっても代入できる(高い包容力)。
3. ただし、オブジェクトリテラルを直接渡すときは「タイポ防止の厳格なチェック」が働くので注意。
4. どうしても厳密に区別したいときは、ブランド型などのテクニックで制御できる。

TypeScriptのコンパイラは、あなたの書いたコードの「構造」をいつも細かくチェックしてくれている優秀な審査員のようなものです。
「名前が違うからダメ!」と冷たく突き放すのではなく、「要求されているデータを持ってる?」という視点でコードを見てあげると、TypeScriptとの対話が何倍も楽しくなりますよ。

ここをクリアできれば、TypeScriptの基本はもうバッチリマスターです!自信を持って次のステップへ進んでいきましょう。それでは、また次回の記事でお会いしましょう!

タイトルとURLをコピーしました