【入門編】TypeScriptの「構造的部分型」が配列操作に与える影響と注意点 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。
普段、他のプログラミング言語(JavaやC#、あるいは動的型付き言語など)からTypeScriptに入ってきたとき、最初に「おや?」とつまずきやすいポイントがいくつかあります。その代表格が、今回お話しする「構造的部分型(Structural Subtyping)」という仕組みです。

「配列やオブジェクトの型チェックが、なんだか自分の思っている挙動と違うな……?」と感じたことはありませんか?
ここをクリアすれば、TypeScriptの型システムの「心臓部」を理解したも同然です。さあ、一緒に基本から本質まで優しく紐解いていきましょう!

—

1. そもそも「構造的部分型」ってなに?

多くの言語(例えばJavaやC#など)で採用されているのは、「公称型(Nominal Subtyping)」という仕組みです。これは、「名前が同じでなければ別物」という厳格なルールです。人間社会で言えば、「社員証に書かれた名前や所属が完全に一致していないとダメ」という世界ですね。

一方、TypeScriptが採用しているのは「構造的部分型」です。
これは、「中身(プロパティや構造)が満たされていれば、同じものとみなすよ」という、実用主義なルールです。

> 【イメージ図解】
> 公称型(Javaなどの世界):
> `class Dog` と `class Cat` は、たとえ中身がどちらも「鳴く機能」を持っていたとしても、名前が違うから別物!
> 構造的部分型(TypeScriptの世界):
> 「お前、目があって、耳があって、猫撫で声を出せるなら、それはもう『猫』として扱うぜ!」という中身重視の世界。

この考え方は、オブジェクトだけでなく、配列やタプルの操作にも大きな影響を与えます。次のセクションで、具体的なコードを見てみましょう。

—

2. 配列とタプルで起きる「予期せぬ挙動」の正体

TypeScriptの配列(`T[]`)やタプル(`[A, B]`)を扱うとき、この構造的部分型を知らないと「えっ、なんでエラーにならないの?」「逆に、なんでここでエラーになるの?」と混乱してしまいます。

よくある具体例をいくつか見てみましょう。

例1:タプルは「配列の親戚」として扱われる

まずは、固定長の配列である「タプル」と、可変長の「通常配列」の関係を見てみます。

// 2つの数値を持つタプル型
type Point2D = [number, number];

// 通常の数値の配列
let numbers: number[] = [1, 2, 3, 4, 5];

// さあ、ここで代入できるでしょうか?
let p: Point2D = numbers; // ❌ コンパイルエラーになります!

【なぜエラーになるの?】
コンパイラ(TypeScriptの頭脳)の視点になって考えてみましょう。
`Point2D` は「絶対に要素が2つである」という構造を期待しています。しかし、`number[]` は「要素が何個入っているか分からない(0個かもしれないし、100個あるかもしれない)」可変長の構造です。

つまり、`Point2D` が要求する「厳密な2つの要素」という構造を、`number[]` は保証できません。そのため、「安全ではない」と判断されて弾かれるわけです。

では、逆はどうでしょうか?

let p: Point2D = [10, 20];
let numbers: number[] = p; // ⭕️ これはエラーになりません!

「2つの数字が入ったタプル」は、「数字が並んだ配列」としての要件(数字がインデックス順に入っていること)を完全に満たしているため、構造的部分型により代入が許可されます。ここがポイントですね!

—

例2:オブジェクトの配列における「余分なプロパティ」の罠

次は、オブジェクトの配列を扱うときによくあるケースです。構造的部分型は、「要求されているプロパティさえ持っていれば、余分なものがあってもOK」という特徴があります。

interface User {
id: number;
name: string;
}

// 管理者データ(idとnameの他に、admin権限を持っている)
const admins = [
{ id: 1, name: “Alice”, role: “superadmin” },
{ id: 2, name: “Bob”, role: “admin” },
];

// User型の配列を引数にとる関数
function printUsers(users: User[]) {
users.forEach(u => console.log(u.name));
}

// これはエラーになるでしょうか?
printUsers(admins); // ⭕️ なんと、エラーにならずに動きます!

【ここで何が起きているの?】
`printUsers` 関数は「`id` と `name` さえ持っていればいいよ(`User[]`)」と言っています。
渡された `admins` の中身を見ると、`role: “superadmin”` という余分なプロパティを持っていますが、肝心の `id` と `name` はしっかり持っていますよね。

TypeScriptの構造的部分型では、「必要な最小限の構造(Contract)」を満たしていれば、余分なガジェット(プロパティ)がついていても「OK」とみなされます。これにより、APIから受け取ったリッチなオブジェクトを、シンプルな関数にそのままポンと渡すことができて非常に便利なんです。

—

3. 実務でハマりやすい「型アサーション」と「readonly」の注意点

構造DP(構造的部分型)の便利さに甘えていると、実務で痛い目を見る瞬間があります。特に注意すべき2つの罠をシェアしますね。

注意点①:関数の引数の「双方向性(Bivariance)」

少し高度ですが、配列のメソッド(`map` や `forEach` など)に渡すコールバック関数の引数でも、構造的部分型が働きます。

type Animal = { name: string };
type Dog = { name: string; bark: () => void };

function processAnimals(animals: Animal[], callback: (a: Animal) => void) {
animals.forEach(callback);
}

const myDogs: Dog[] = [
{ name: “ポチ”, bark: () => console.log(“ワン”) }
];

// Animalを受け取るはずの関数に、Dog専用の処理を渡そうとする
processAnimals(myDogs, (dog: Dog) => {
dog.bark(); // 構造的部分型の仕様により、TypeScriptはこの記述を許容することがあります
});

一見便利に見えますが、もし `animals` の中に `Dog` ではない、ただの `Animal`(`bark`を持たない猫など)が混ざっていた場合、実行時に `dog.bark is not a function` というお馴染みのクラッシュ(TypeError)を引き起こします。
構造的部分型は「コンパイル時の安全性」を劇的に上げてくれますが、実行時のデータ構造の完全性までは完全に保証してくれないという点を心に留めておきましょう。

注意点②:readonly(読み取り専用)配列の壁

配列をイミュータブル(変更不可)に扱うための `readonly number[]` や `ReadonlyArray` も、構造的部分型において厳格にチェックされます。

let mutableNums: number[] = [1, 2, 3];
let readonlyNums: readonly number[] = mutableNums; // ⭕️ セーフ(書き換え可能なものを、読み取り専用として扱う分には安全)

// 逆はどうか?
// mutableNums = readonlyNums; // ❌ 爆弾エラー!

【理由】
`readonlyNums` は「絶対に書き換えられない」と保証されている配列です。それを `mutableNums`(いつでも `push` で書き換えられる配列)の変数に入れてしまうと、`mutableNums.push(4)` などで `readonlyNums` の中身まで書き換えられてしまう可能性が生まれます。
TypeScriptは「安全なものから危険なものへの格上げ」を断固として拒否します。この非対称性も、構造的部分型と型の安全性を両立させるための重要なルールです。

—

まとめ:ここをクリアすればTypeScriptは怖くない!

今回は、TypeScriptの「構造的部分型」が配列やタプル、オブジェクトの操作にどう影響するのかを見てきました。

  • 構造的部分型とは: 名前ではなく「中身(構造)」で型の互換性を判定する仕組み。
  • 配列・タプルの注意点:
  • 厳格な構造(タプル)へ、緩い構造(通常配列)は代入できない。
  • 必要なプロパティさえあれば、余分なプロパティを持っていても代入できる(オブジェクト配列)。
  • 本質の理解: コンパイラが「安全かどうか」をどう判断しているのかを頭の中でシミュレーションできるようになると、エラーメッセージに怯えることがなくなります。

ここをしっかりと自分のものにできれば、TypeScriptの基本はバッチリマスターできていますよ!自信を持って次のステップへ進んでくださいね。
それでは、快適なTypeScriptライフを!

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