インターフェースと型エイリアス
レッスン5:インターフェースと型エイリアス
このレッスンで学ぶこと
interfaceとtypeの違いを理解し、使い分けられる- インターフェースを拡張できる
- 構造的型付けの考え方を説明できる
- 読み取り専用プロパティを扱える
レッスン4 では、関数の引数と戻り値に型注釈を付ける方法を学びました。ここまでのレッスンで、オブジェクトの型注釈は { name: string; age: number } のようにインラインで書いてきましたが、レッスン2 でも触れたとおり、同じ形を何度も書くのは手間がかかります。このレッスンでは、オブジェクトの形に名前を付けて使い回す 2 つの方法、interface と type を学びます。
名前を付けるという行為は、単にタイピングの文字数を減らすためのものではありません。「ユーザーというものは、名前と年齢を持つ」という認識をコードの中にはっきりと固定し、その認識をチーム全体、あるいは数か月後の自分と共有するための手段です。名前が付いた形は、コードの中でひとつの共通言語として自然に機能し始めます。
interfaceでオブジェクトの形に名前を付ける
interface は、オブジェクトの形に名前を付けるための構文です。
interface User {
name: string;
age: number;
}
const user: User = {
name: "森田 彩花",
age: 30
};
interface User { ... } で「User という形」を定義し、user: User でその形を変数に適用しています。レッスン2 で書いた { name: string; age: number } というインラインの型注釈と、意味はまったく同じです。名前が付いたことで、同じ形のオブジェクトを扱う別の場所でも、User という名前を書くだけで済むようになります。
function introduce(user: User): string {
return `私は${user.name}、${user.age}歳です。`;
}
function isAdult(user: User): boolean {
return user.age >= 18;
}
introduce と isAdult のどちらも、引数の型として User を再利用しています。もし User の形を変更する必要が出てきても、interface User の定義を 1 か所直すだけで、それを使うすべての関数に変更が反映されます。
typeでも同じことができる——型エイリアス
TypeScript には、interface とよく似た働きをする type(型エイリアス)という構文もあります。
type User = {
name: string;
age: number;
};
const user: User = {
name: "森田 彩花",
age: 30
};
見た目は interface と {} の代わりに type と = を使う点が違うだけで、できることはほぼ同じです。実はレッスン3 の実習ですでに、type OrderStatus = "pending" | "shipped" | "completed"; という形で型エイリアスを使っていました。type は、オブジェクトの形だけでなく、ユニオン型やリテラル型のような「オブジェクト以外の型」にも名前を付けられる点が特徴です。
type UserId = string | number;
type Size = "small" | "medium" | "large";
const id: UserId = "u-001";
const size: Size = "medium";
interface はオブジェクトと関数の形を表すことに特化した構文で、UserId や Size のようなユニオン型には使えません。「オブジェクトの形を表したいだけなら interface」「ユニオン型やリテラル型など、オブジェクト以外にも名前を付けたいなら type」というのが、本コースでの基本の使い分けです。
💡 ポイント 現場のコードでは、プロジェクトによって「オブジェクトの型は基本的に
interfaceで統一する」「すべてtypeで統一する」など、方針が異なります。どちらか一方が絶対的に正しいわけではないため、参加するプロジェクトの慣習に合わせるのが実務的な判断です。本コースでは、役割を分けて使う考え方を土台として身につけます。
interfaceの拡張——既存の形を土台にする
interface には、既存のインターフェースを土台にして、プロパティを追加した新しい形を作る「拡張」という機能があります。extends というキーワードを使います。
interface User {
name: string;
age: number;
}
interface AdminUser extends User {
permissions: string[];
}
const admin: AdminUser = {
name: "森田 彩花",
age: 30,
permissions: ["記事の編集", "会員の管理"]
};
AdminUser は User を拡張し、permissions というプロパティを追加した形です。admin には User が持つ name・age と、AdminUser で追加した permissions の、すべてのプロパティが必要になります。共通する部分を 1 つのインターフェースにまとめておき、必要な場面だけ拡張する、という設計ができます。
拡張は複数の段階に重ねることもできます。
interface SuperAdminUser extends AdminUser {
canDeleteAccounts: boolean;
}
SuperAdminUser は、AdminUser の permissions と、その元になった User の name・age、そして新しく追加した canDeleteAccounts を、すべて持つ形になります。
インターフェースを組み合わせる——プロパティの型に別のインターフェースを使う
インターフェースのプロパティには、string や number といった基本の型だけでなく、別のインターフェースを指定することもできます。
interface Author {
name: string;
role: string;
}
interface Article {
title: string;
author: Author;
}
const article: Article = {
title: "TypeScript入門ガイド",
author: {
name: "森田 彩花",
role: "講師"
}
};
console.log(article.author.name);
Article の author プロパティは、Author というインターフェースの形を持つオブジェクトです。プロパティの型として、基本の型だけでなく、自分で定義したインターフェースを重ねて使うことで、現実のデータが持つ「入れ子になった構造」を、そのまま型として表現できます。「記事には著者がいて、著者には名前と役割がある」という関係が、コードを読むだけで伝わるようになります。ブログ記事、コメント一覧、注文情報のように、複数の情報が組み合わさったデータを扱うときほど、この組み合わせ方が役立ちます。
typeで似たことをする——交差型
type にも、interface の extends に近いことを実現する書き方があります。アンパサンド & でつないだ「交差型」です。
type User = {
name: string;
age: number;
};
type AdminUser = User & {
permissions: string[];
};
const admin: AdminUser = {
name: "森田 彩花",
age: 30,
permissions: ["記事の編集", "会員の管理"]
};
User & { permissions: string[] } は、「User の形と、permissions を持つ形の、両方を満たす形」を表します。結果として、extends で作った AdminUser とほぼ同じ形になります。interface の拡張と type の交差型は、書き方こそ違いますが、「既存の形を組み合わせて新しい形を作る」という目的は共通しています。
📝 補足 迷ったときの目安として、オブジェクトの形を段階的に拡張していく設計が中心なら
interfaceのextends、複数の独立した形を組み合わせる場面が多いならtypeの交差型、という使い分けが一般的です。本コースでは、まずinterfaceとextendsの組み合わせに慣れておけば十分です。どちらの書き方も、最終的にコンパイラが確認する「求められる形」自体は変わりません。
構造的型付け——形が合っていれば通る
TypeScript の型システムには、「構造的型付け」という重要な考え方があります。ある値が特定のインターフェースに合っているかどうかは、インターフェースの名前ではなく、実際に持っているプロパティの形だけで判断されます。
interface Point {
x: number;
y: number;
}
function printPoint(point: Point) {
console.log(`(${point.x}, ${point.y})`);
}
const location = { x: 10, y: 20, z: 30 };
printPoint(location);
location は Point というインターフェースを明示的に指定して作られたオブジェクトではありません。それでも、x と y という Point が求めるプロパティを両方持っているため、printPoint にそのまま渡せます。z という余分なプロパティを持っていても問題になりません。
💡 ポイント 中核メッセージの 1 つである「インターフェースは『この形をしていること』だけを求める」は、まさにこの構造的型付けを指しています。名前が一致している必要はなく、必要なプロパティさえそろっていれば、TypeScript はその値を受け入れます。
ただし、変数を宣言すると同時にオブジェクトを渡す場合は、少し異なる挙動になります。
function printPoint(point: Point) {
console.log(`(${point.x}, ${point.y})`);
}
printPoint({ x: 10, y: 20, z: 30 }); // エラー
error TS2345: Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
その場で直接書いたオブジェクト(オブジェクトリテラル)を関数に渡す場合、TypeScript は「余分なプロパティが紛れ込んでいないか」を追加でチェックします。これは、プロパティ名を綴り間違えたまま渡してしまう事故を防ぐための仕組みです。変数に一度入れてから渡す場合はこのチェックが働かないため、構造的型付けの基本ルール(形が合っていれば通る)がそのまま適用されます。
構造的型付けは、ほかの多くのプログラミング言語が採用している「名前による型付け」とは考え方が異なります。名前による型付けでは、「Point として宣言されたものだけが Point として扱われる」という厳格さがありますが、TypeScript では、必要な形さえ持っていれば、宣言方法を問わず受け入れられます。この柔軟さは、既存の JavaScript のコードに後から型を足していくときに大きな助けになります。レッスン7 で扱う既存コードへの型の導入でも、この構造的型付けの柔軟さを活用します。
読み取り専用プロパティ——書き換えを防ぐ
プロパティの中には、一度設定したら変更してほしくないものがあります。readonly を付けると、そのプロパティへの再代入をコンパイルの段階で防げます。
interface User {
readonly id: string;
name: string;
age: number;
}
const user: User = {
id: "u-001",
name: "森田 彩花",
age: 30
};
user.name = "森田 彩花(更新)"; // 問題なく変更できる
user.id = "u-002"; // エラー:readonly のため変更できない
error TS2540: Cannot assign to 'id' because it is a read-only property.
会員 ID や商品コードのように、「作成時に決まったら、以後は変わらないはず」の値に readonly を付けておくと、うっかり上書きしてしまう間違いを防げます。レッスン2 で学んだ const が「変数への再代入」を防ぐのに対して、readonly は「オブジェクトの特定のプロパティへの再代入」を防ぐ役割を持ちます。
配列そのものを読み取り専用にしたいときは、readonly を配列の型注釈の前に付けます。
function printScores(scores: readonly number[]) {
console.log(scores.join(", "));
scores.push(100); // エラー:readonly 配列には push できない
}
error TS2339: Property 'push' does not exist on type 'readonly number[]'.
readonly number[] は、「中身を並べて見ることはできるが、要素を追加・削除・置き換えすることはできない配列」を表します。関数に配列を渡すとき、「この関数は渡された配列を書き換えない」という約束を、型注釈だけで表現できるのは大きな利点です。呼び出す側は、関数の中でうっかり元の配列が変更されてしまう心配をせずに済みます。
実習:ToDoアプリのデータ構造に型をつける
「JavaScript 入門」の最終レッスンで作った ToDo リストのデータ構造に、ここまで学んだ内容で型を付けてみましょう。
interface Todo {
readonly id: number;
text: string;
done: boolean;
}
const todos: Todo[] = [];
function addTodo(text: string): Todo {
const newTodo: Todo = {
id: todos.length + 1,
text: text,
done: false
};
todos.push(newTodo);
return newTodo;
}
function completeTodo(id: number): void {
const target = todos.find((todo) => todo.id === id);
if (target !== undefined) {
target.done = true;
}
}
addTodo("牛乳を買う");
addTodo("掃除をする");
completeTodo(1);
todos.forEach((todo) => {
const mark = todo.done ? "[完了]" : "[未完了]";
console.log(`${mark} ${todo.text}`);
});
Todo インターフェースでは、id を readonly にして作成後の書き換えを防ぎ、text と done は後から変更できる通常のプロパティとして定義しています。todos: Todo[] と書くだけで、配列に入れられるのは Todo の形をしたオブジェクトだけだと保証されます。もとの JavaScript のコードでは、todos の中身がどんな形をしているかはコードを読むまでわかりませんでしたが、型を付けたことで、Todo インターフェースを見るだけで構造が伝わるようになりました。
addTodo の戻り値の型を Todo にしているのも意図的です。関数を呼び出した側は、戻ってきた値がどんな形をしているかを、関数の中身を追わなくても把握できます。completeTodo の中で使っている find は、レッスン3 で扱ったとおり見つからない場合に undefined を返すため、target !== undefined で型を絞り込んでから target.done を書き換えています。ユニオン型・関数の型・インターフェースという、レッスン3 から 5 までの内容が、この短いコードの中に一通りそろっています。
まとめ
このレッスンでは、以下のことを学びました。
interfaceはオブジェクトや関数の形に名前を付ける構文type(型エイリアス)は、オブジェクトに加えてユニオン型やリテラル型にも名前を付けられる、より幅広い構文interfaceはextendsで拡張し、既存の形を土台に新しい形を作れる- TypeScript は構造的型付けを採用しており、名前ではなく実際に持つ形でインターフェースに合っているかを判断する
readonlyを付けたプロパティは、作成後の再代入をコンパイルの段階で確実に防げる
インターフェースと型エイリアスは、ここまでのレッスンで学んだ型注釈・型推論・ユニオン型を、まとめて名前の付いた 1 つの形としてわかりやすく扱うための道具です。次のレッスンでは、いよいよ「JavaScript 入門」が扱っていない領域である class とモジュールを学びます。大きくなったコードを、意味のある単位にきちんと分割する方法を丁寧に見ていきましょう。
確認クイズ
このレッスンの理解度をチェックしましょう。