TypeScript:type と interface
1学習の目的
- type と interface で型に名前を付けられるようになる。同じ型定義を何度も書く必要がなくなり、名前そのものが意味の説明になる。
- 型を合成・拡張できるようになる。既存の型に項目を足したり、複数の型を組み合わせたりして、実務のデータ構造を無理なく表現できるようになる。
2基礎解説
TS03で { name: string; price: number }[] のような長い型を書きました。同じ型を何度も書くのは非効率です。名前を付けて再利用しましょう。
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| 型に名前を付ける | type Product = { … } | 何にでも使える |
| オブジェクトの形を定義 | interface Product { … } | オブジェクト専用 |
| 型を合成する | type A = B & { extra: string } | 両方の性質を持つ |
| 型を拡張する | interface A extends B { … } | interface の書き方 |
- type は何にでも名前を付けられる。オブジェクトだけでなく、ユニオン型(TS06)や関数の型にも使える。
- interface はオブジェクトの形を定義するためのもの。ユニオン型には使えないが、オブジェクトを表す場合は type とほぼ同じように書ける。
- 迷ったら type でよい。使える範囲が広く、書き方も統一できる。interface はライブラリの型定義やクラスとの組み合わせでよく使われる。
- & で型を合成できる。Product & { stock: number } は「Productの全項目 + stock」を持つ型になる。
- 型は入れ子にできる。type Order = { customer: Customer; items: Product[] } のように、名前を付けた型を部品として組み合わせられる。
現場使用例:APIレスポンスの型定義、共通で使うデータ構造、複数の関数で受け渡すオブジェクト。型に名前が付くと、コードを読む人が構造を一目で把握できる。
// type で名前を付ける
type Product = {
id: number;
name: string;
price: number;
};
// 何度でも使い回せる
const item: Product = { id: 1, name: "ノートPC", price: 128000 };
const items: Product[] = [item];
function format(p: Product): string {
return `${p.name}:${p.price.toLocaleString()}円`;
}
// interface でも同じことができる
interface Customer {
id: number;
name: string;
area: string;
}
// & で型を合成する(両方の項目を持つ)
type StockProduct = Product & { stock: number };
const sp: StockProduct = {
id: 1, name: "ノートPC", price: 128000, stock: 5,
};
// interface は extends で拡張する
interface VipCustomer extends Customer {
rank: string;
}
// 型を部品として組み合わせる
type Order = {
id: number;
customer: Customer;
items: Product[];
};
3基本ドリル(10問)
出力 == ノートPC:128,000円
type Product = { id: number; name: string; price: number }; と書く。
type Product = {
id: number;
name: string;
price: number;
};
const item: Product = { id: 1, name: "ノートPC", price: 128000 };
console.log(`${item.name}:${item.price.toLocaleString()}円`);
型に名前が付くと、以降は Product と書くだけで済む。TS03で毎回書いていた長い型定義から解放される。
A. 完全に同じ B. type はユニオン型にも使えるが interface はオブジェクト専用 C. interface のほうが高速 D. type は非推奨 選択★☆☆無料
解答 == B
ユニオン型に名前を付けられるのはどちらか。
B
type は使える範囲が広い。type Status = "A" | "B"; は書けるが、interface では書けない。迷ったら type を使えばよい。
出力 == 田中商事(東京)
interface Customer { id: number; name: string; area: string } と書く(イコールは不要)。
interface Customer {
id: number;
name: string;
area: string;
}
const c: Customer = { id: 1, name: "田中商事", area: "東京" };
console.log(`${c.name}(${c.area})`);
interface にはイコールが要らないのが type との見た目の違い。中身の書き方はほぼ同じ。
____ Point = { x: number; y: number };
const p: Point = { x: 3, y: 4 };
console.log(p.x + p.y);
出力 == 7
型に別名を付けるキーワード。小文字4文字。
type
type は「型に名前を付ける」ためのキーワード。オブジェクト・ユニオン型・関数の型など、どんな型にも使える。
A. B か extra のどちらか B. B の全項目に加えて extra も持つ C. B から extra を除く D. エラーになる 選択★☆☆無料
解答 == B
& は「かつ」を表す。
B
& は交差型と呼ばれ、両方の性質を併せ持つ型を作る。|(どちらか)とは逆の意味になる。
出力 == ノートPC:在庫5個
type StockProduct = Product & { stock: number }; と書く。
type Product = {
id: number;
name: string;
price: number;
};
type StockProduct = Product & { stock: number };
const item: StockProduct = {
id: 1,
name: "ノートPC",
price: 128000,
stock: 5,
};
console.log(`${item.name}:在庫${item.stock}個`);
既存の型を土台にして新しい型を作れる。Product が変われば StockProduct も自動的に追随するので、修正漏れが起きない。
interface Base { id: number }
interface Item ____ Base {
name: string;
}
const i: Item = { id: 1, name: "A" };
console.log(i.id, i.name);
出力 == 1 A
「拡張する」を意味する7文字のキーワード。
extends
interface は extends で拡張する。type の & とほぼ同じ結果になるが、書き方が違う。
A. type のみ B. interface のみ C. どちらも使える D. どちらも使えない 選択★★☆無料
解答 == A
interface はオブジェクトの形を定義するもの。
A
interface はオブジェクト以外に名前を付けられない。TS06で書いた type Status = … は、type でなければ書けなかった。
出力2行。ノートPC(128,000円) / マウス(3,200円)
関数の引数の型に Product を使う。for...of で回す。
type Product = {
id: number;
name: string;
price: number;
};
function label(p: Product): string {
return `${p.name}(${p.price.toLocaleString()}円)`;
}
const items: Product[] = [
{ id: 1, name: "ノートPC", price: 128000 },
{ id: 2, name: "マウス", price: 3200 },
];
for (const item of items) {
console.log(label(item));
}
関数の引数・配列の要素・変数の型、すべてに同じ名前を使える。型名が処理の全体に一貫性を与えている。
出力 == 注文1:田中商事(2点)
型の中で他の型を使える。items は Product[] と書く。
type Customer = { id: number; name: string };
type Product = { id: number; name: string; price: number };
type Order = {
id: number;
customer: Customer;
items: Product[];
};
const order: Order = {
id: 1,
customer: { id: 1, name: "田中商事" },
items: [
{ id: 1, name: "ノートPC", price: 128000 },
{ id: 2, name: "マウス", price: 3200 },
],
};
console.log(`注文${order.id}:${order.customer.name}(${order.items.length}点)`);
型を部品として組み合わせる——TS03で入れ子の型を長々と書いた課題が、こうして整理できる。実務のデータ構造はこの形で表現する。
4実践シナリオ(5問)
出力 == ノートPC, モニター
型を1回定義すれば、配列にも filter の中でも使える。
type Product = {
id: number;
name: string;
price: number;
category: string;
};
const products: Product[] = [
{ id: 1, name: "ノートPC", price: 128000, category: "PC" },
{ id: 2, name: "マウス", price: 3200, category: "周辺機器" },
{ id: 3, name: "モニター", price: 45000, category: "PC" },
];
const pcs = products.filter((p) => p.category === "PC");
console.log(pcs.map((p) => p.name).join(", "));
型定義は1箇所、使用は何箇所でも。もし category を数値に変える必要が出ても、型定義の1行を直せば、使っている全箇所でエラーが出て修正漏れを防げる。
出力2行が完全一致
type InventoryItem = Product & { stock: number; location: string }; と書く。
type Product = {
id: number;
name: string;
price: number;
};
type InventoryItem = Product & {
stock: number;
location: string;
};
const items: InventoryItem[] = [
{ id: 1, name: "ノートPC", price: 128000, stock: 5, location: "棚A" },
{ id: 2, name: "マウス", price: 3200, stock: 42, location: "棚B" },
];
for (const item of items) {
console.log(`${item.name}:${item.stock}個(${item.location})`);
}
商品情報と在庫情報を分けて定義し、必要な場面で合成している。カタログでは Product、倉庫では InventoryItem と使い分けられる。
出力 == 未発送合計:128,000円
TS06のリテラル型を type で定義し、Order の中で使う。filter と reduce で集計する。
type Status = "未発送" | "発送済";
type Order = {
id: number;
status: Status;
amount: number;
};
const orders: Order[] = [
{ id: 1, status: "未発送", amount: 128000 },
{ id: 2, status: "発送済", amount: 45000 },
];
const total = orders
.filter((o) => o.status === "未発送")
.reduce((sum, o) => sum + o.amount, 0);
console.log(`未発送合計:${total.toLocaleString()}円`);
リテラル型を型の部品として使っている。o.status === "未発送中" のような打ち間違いは、比較した時点でエラーになる。
出力2行が完全一致
interface VipCustomer extends Customer { rank: string } と書く。VIPだけ ★rank を付ける。
interface Customer {
id: number;
name: string;
area: string;
}
interface VipCustomer extends Customer {
rank: string;
}
const normal: Customer = { id: 1, name: "田中商事", area: "東京" };
const vip: VipCustomer = { id: 2, name: "鈴木工業", area: "大阪", rank: "S" };
console.log(`${normal.name}(${normal.area})`);
console.log(`${vip.name}(${vip.area})★${vip.rank}`);
VipCustomer は Customer としても扱える。Customer を引数に取る関数に VipCustomer を渡せるので、共通処理はまとめて書ける。
「ノートPC × 2 = 256,000円」
「マウス × 5 = 16,000円」
「キーボード × 3 = 25,500円」
「合計:297,500円」 コーディング★★★無料
出力4行が完全一致
OrderLine の中に Product を入れ子にする。金額は line.product.price * line.qty。
type Product = {
id: number;
name: string;
price: number;
};
type OrderLine = {
product: Product;
qty: number;
};
const lines: OrderLine[] = [
{ product: { id: 1, name: "ノートPC", price: 128000 }, qty: 2 },
{ product: { id: 2, name: "マウス", price: 3200 }, qty: 5 },
{ product: { id: 3, name: "キーボード", price: 8500 }, qty: 3 },
];
let total = 0;
for (const line of lines) {
const amount = line.product.price * line.qty;
total += amount;
console.log(
`${line.product.name} × ${line.qty} = ${amount.toLocaleString()}円`
);
}
console.log(`合計:${total.toLocaleString()}円`);
型の入れ子が、データの入れ子をそのまま表している。line.product.name のように辿れるのは、型が構造を知っているから。打ち間違えれば即座に分かる。
5仕上げ課題
定義する型
① Category:「PC」「周辺機器」に限定(リテラル型)
② Product:{ id: number; name: string; price: number; category: Category }
③ Customer:{ id: number; name: string; area: string }
④ VipCustomer:Customer に discountRate: number を足した型(& で合成)
⑤ OrderLine:{ product: Product; qty: number }
⑥ Order:{ id: number; customer: VipCustomer; lines: OrderLine[] }
データ:VIP顧客「田中商事(東京・割引率0.1)」が、ノートPC(128000/PC)×2 と マウス(3200/周辺機器)×5 を注文
処理:明細を出力し、小計・割引・合計を計算する
期待される出力(5行)
田中商事(東京)様のご注文
[PC] ノートPC × 2 = 256,000円
[周辺機器] マウス × 5 = 16,000円
小計:272,000円
合計:244,800円(割引27,200円)
出力5行が完全一致
6つの型を順に定義してから、データを作る。割引額は Math.round(小計 * discountRate)。カテゴリは line.product.category で取り出せる。
type Category = "PC" | "周辺機器";
type Product = {
id: number;
name: string;
price: number;
category: Category;
};
type Customer = {
id: number;
name: string;
area: string;
};
type VipCustomer = Customer & { discountRate: number };
type OrderLine = {
product: Product;
qty: number;
};
type Order = {
id: number;
customer: VipCustomer;
lines: OrderLine[];
};
const order: Order = {
id: 1,
customer: { id: 1, name: "田中商事", area: "東京", discountRate: 0.1 },
lines: [
{
product: { id: 1, name: "ノートPC", price: 128000, category: "PC" },
qty: 2,
},
{
product: { id: 2, name: "マウス", price: 3200, category: "周辺機器" },
qty: 5,
},
],
};
console.log(`${order.customer.name}(${order.customer.area})様のご注文`);
let subtotal = 0;
for (const line of order.lines) {
const amount = line.product.price * line.qty;
subtotal += amount;
console.log(
`[${line.product.category}] ${line.product.name} × ${line.qty} = ${amount.toLocaleString()}円`
);
}
const discount = Math.round(subtotal * order.customer.discountRate);
const total = subtotal - discount;
console.log(`小計:${subtotal.toLocaleString()}円`);
console.log(
`合計:${total.toLocaleString()}円(割引${discount.toLocaleString()}円)`
);
6つの型が組み合わさって、ECサイトのデータ構造を表現している。注目してほしいのは、型定義を読むだけでシステムの全体像が分かることだ。「注文には顧客と明細があり、明細には商品と数量がある」——この関係が、コードを1行も読まずに把握できる。
VipCustomer = Customer & { discountRate: number } という合成も効いている。もし Customer に電話番号を追加したくなったら、1箇所直すだけで VipCustomer にも自動的に反映される。同じ項目を2箇所に書いていたら、必ずどこかで食い違いが生まれていた。
そして Category をリテラル型にしたことで、category: "PC"(全角)のような打ち間違いも防げる。型は小さな部品から組み上げるほど、守備範囲が広がる。
ただし今の設計には弱点がある。すべての項目が必須なので、「まだ住所が未登録の顧客」を表現できない。次章のオプショナルと null 安全を学ぶと、この「あるかもしれない項目」を正しく扱えるようになる。