05. 構造的型付け

名前ではなく「形」で互換を判定する / この資料の山場①

📅 作成: 2026-08-29 / 更新: 2026-08-29

この章の到達点

この章の内容

  1. 形が同じなら同じ型として通る
  2. 余剰プロパティチェック
  3. クラスも形で判定される
  4. 意図的に区別する — ブランド型

05.1 形が同じなら同じ型として通る

名前は見ていない

TypeScript は型の名前を見ません。持っているプロパティの形だけを見ます。

type User = { name: string };
type Animal = { name: string };

const a: User = { name: "Alice" };
const b: Animal = a;              // 通る。形が同じだから

UserAnimal はまったく別の意図で作った型ですが、形が同じなので互換です。

名前的型付け(Java・C#) 型の名前が一致しないと互換にならない class User name: String class Animal name: String × 形が同じでも代入できない 互換にするには implements や extends が要る 構造的型付け(TypeScript) 形が一致すれば互換になる type User name: string type Animal name: string OK 宣言なしで代入できる 互いを知らなくても、形が合えば通る 判定の基準 「受け取る側が要求するプロパティを、渡す側がすべて持っているか」だけを見る
図 1 — 名前で判定するか、形で判定するか

多い分には構わない

判定の基準は「受け取る側が要求するものを、渡す側が持っているか」です。余分に持っていても通ります。

type Named = { name: string };

const user = { name: "Alice", age: 30, email: "a@example.com" };
const n: Named = user;            // 通る。name を持っているから

Java・C# から来た人へ

これは Java・C# の名前的型付け(nominal typing)と根本的に違います。Java では、形が同じでも implements を書かなければ互換になりません。

TypeScript が構造的型付けを採用しているのは、JavaScript のコードに後から型を付けるためです。既存の JavaScript は「この型を実装します」とは宣言していません。形だけを見る方式なら、宣言を足さずに型を付けられます。

関数も形で判定される

type Handler = (x: number) => void;

const f: Handler = (x) => console.log(x);          // OK
const g: Handler = () => console.log("hi");        // OK — 引数を使わなくてよい
const h: Handler = (x: number, y: number) => {};   // エラー — 引数が多い

引数は少ない分には構いません。JavaScript では、渡された引数を使わない関数は普通に書けるためです([1,2,3].map(x => x * 2) のように、map が渡す2つ目・3つ目の引数を無視できます)。この扱いは 07. 関数の型 で詳しく見ます。

05.2 余剰プロパティチェック

直接書いたときだけ厳しくなる

前の節で「余分に持っていても通る」と書きましたが、例外があります。

type User = { name: string };

// ① 変数を経由する → 通る
const obj = { name: "Alice", age: 30 };
const a: User = obj;

// ② オブジェクトリテラルを直接書く → エラー
const b: User = { name: "Alice", age: 30 };
//                               ~~~~~~~
// error TS2353: Object literal may only specify known properties,
//               and 'age' does not exist in type 'User'.
//
// 訳: オブジェクト リテラルは既知のプロパティのみ指定できます。
//     'age' は型 'User' に存在しません。

同じ形なのに、①は通り、②はエラーになります。これが余剰プロパティチェックです。

① 変数を経由する — 通る const obj = { name, age } いったん変数に入れる 形の判定だけ name を持っているか const a: User = obj; age が余っていても通る ② オブジェクトリテラルを直接書く — エラー { name, age } をその場で 変数を経由しない 余剰プロパティ チェックが働く error TS2353 age は User に存在しない 同じ形なのに結果が違う。②を厳しくしているのは打ち間違いを見つけるため
図 2 — 余剰プロパティチェックは、その場で書いたリテラルにだけ働く

なぜこの仕様があるか

打ち間違いを検出するためです。

構造的型付けでは「多い分には構わない」ため、{ namae: "Alice" } のような打ち間違いも「余分なプロパティ」として通ってしまいます。それでは型を書く意味がありません。

そこで「その場で書いたオブジェクトリテラル」に限って、余分なプロパティをエラーにします。変数を経由する場合は、書き手が意図して渡していると見なして通します。

意図的に余分な値を渡したいとき

type User = { name: string };

// ① 変数に入れてから渡す(最も素直)
const payload = { name: "Alice", age: 30 };
const a: User = payload;

// ② 型を union にする
type UserWithExtra = User & { age?: number };

// ③ インデックスシグネチャを足す
type Loose = { name: string; [key: string]: unknown };

as User で黙らせることもできますが、打ち間違いも一緒に通してしまいます06)。

05.3 クラスも形で判定される

implements を書かなくても互換

interface Logger {
	log(message: string): void;
}

// implements を書いていない
class ConsoleLogger {
	log(message: string) {
		console.log(message);
	}
}

const l: Logger = new ConsoleLogger();     // 通る

Java・C# から来た人へ

Java なら class ConsoleLogger implements Logger と書かなければコンパイルが通りません。TypeScript では書かなくても通ります。

ただし implements を書く意味はあります。書いておくと、クラス側の実装が要件を満たさなくなったときに、クラスの定義位置でエラーになります。書かない場合、エラーが出るのは「代入しようとした場所」で、原因から離れた位置になります。

class ConsoleLogger implements Logger {   // 書いておくと、ここで守られる
	log(message: string) { console.log(message); }
}

private があると形が変わる

クラスに private なメンバーがあると、同じ形でも互換になりません。

class A { private secret = 1; }
class B { private secret = 1; }

let a: A = new A();
let b: B = new B();
a = b;
//  ~
// error TS2322: Type 'B' is not assignable to type 'A'.
//   Types have separate declarations of a private property 'secret'.
//
// 訳: 型 'B' を型 'A' に割り当てることはできません。
//     プライベート プロパティ 'secret' の宣言が別々です。

private なメンバーは「そのクラスで宣言されたもの」として区別されます。これが、TypeScript で名前的な区別に最も近い挙動です。

05.4 意図的に区別する — ブランド型

形が同じだと混ざる

構造的型付けの弱点は、意味が違うのに形が同じ値を取り違えられることです。

type UserId = string;
type OrderId = string;

function getUser(id: UserId) { /* ... */ }

const orderId: OrderId = "order-123";
getUser(orderId);            // 通ってしまう。どちらも string だから

ブランド型で区別する

実体のないプロパティを足して、形を意図的にずらします。

type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };

function getUser(id: UserId) { /* ... */ }

const orderId = "order-123" as OrderId;
getUser(orderId);
//      ~~~~~~~
// error TS2345: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'.
//
// 訳: 型 'OrderId' の引数を型 'UserId' のパラメーターに割り当てることはできません。

__brand は実行時には存在しません。

型の上でだけ存在する目印です。実際の値はただの文字列なので、実行時のコストはゼロです。as を使わないと作れないため、「ここで意図的に変換した」という箇所が検索できるという副次的な利点もあります。

生成関数と組み合わせる

as を書く場所を1か所に閉じ込めると、検証も一緒に行えます。

// branded.ts
type UserId = string & { readonly __brand: "UserId" };

function toUserId(s: string): UserId {
	if (!s.startsWith("user-")) {
		throw new Error(`UserId の形式が不正です: ${s}`);
	}
	return s as UserId;
}

const id = toUserId("user-001");
console.log(id);

try {
	toUserId("order-123");
} catch (e) {
	console.log((e as Error).message);
}
$ npx tsx branded.ts
user-001
UserId の形式が不正です: order-123

この形は A2. 型レシピ集 にも収めています。

使いどころを絞ってください。

すべての string をブランド型にすると、変換だらけで読みにくくなります。取り違えると実害が出るもの(ID の種類、検証済み/未検証の文字列、単位の違う数値)に限って使います。