表現の改善にAIを活用しています。
あらかじめご了承ください。
extends
TypeScriptの extends は4つの使い方があります。
1.クラスの継承
JavaScriptと同じく、親クラスのプロパティやメソッドを引き継ぎます。継承できる親クラスは1つだけです。
class Animal {
constructor(public name: string) {}
move() {
console.log(`${this.name} が移動した`);
}
}
class Dog extends Animal {
bark() {
console.log("ワン!");
}
}
const d = new Dog("ポチ");
d.move(); // 親のメソッドが使える
d.bark();
2.インターフェースの拡張
既存のインターフェースに項目を追加した新しい型を作ります。クラスと違い、複数を同時に拡張できます。
interface Named {
name: string;
}
interface Aged {
age: number;
}
interface PersonI extends Named, Aged {
email: string;
}
// PersonI は name, age, email を持つ
3.ジェネリクスの制約
型パラメータに「この条件を満たす型だけ受け付ける」という制限をかけます。ここでの A extends B は「継承」ではなく、「A が B に代入可能である」 という意味です。
function getLength<T extends { length: number }>(x: T): number {
return x.length; // T は length を持つことが保証されているので OK
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(123); // エラー:number には length がない
「代入可能」とは
「A が B に代入可能」とは、B 型の変数に A 型の値を入れてもエラーにならないことです。実際にコードで確かめてみましょう。
let withLength: { length: number };
withLength = "hello"; // OK
withLength = [1, 2, 3]; // OK
withLength = 123; // エラー
T extends { length: number } は、この代入が通る型だけを T として受け付けるという条件です。
なぜ string や配列は代入できるのか
型 { length: number } が要求しているのは、「number 型の length プロパティを持っていること」だけです。ほかにどんなプロパティを持っていても構いません。
ではstring 型、配列型、number 型はそれぞれどんなメンバーを持っているのでしょうか。一部を抜き出し書きます。
// string 型のメンバー(抜粋)
{
length: number; // ← ある!
charAt(pos: number): string;
toUpperCase(): string;
slice(start?: number, end?: number): string;
// ...
}
// number[] 型のメンバー(抜粋)
{
length: number; // ← ある!
push(...items: number[]): number;
map<U>(callbackfn: (value: number) => U): U[];
// ...
}
// number 型のメンバー(抜粋)
{
toFixed(fractionDigits?: number): string;
toString(radix?: number): string;
// ... // ← length がない!
}
string 型と配列型は条件を満たしているので代入できます。一方、numberは length を持っていないので代入できません。
string や number はプリミティブ型ですが、プロパティを参照するとき TypeScript はそれぞれ String インターフェース、Number インターフェース(どちらも lib.es5.d.ts)のメンバーとして扱います。上の「string 型のメンバー」「number 型のメンバー」は、これらのインターフェースから抜粋したものです。同様に、配列型 number[] のメンバーは Array<number> インターフェースのものです。
構造的型付け
このようにTypeScriptは「どの型として作られたか」という出自ではなく、「必要なものを持っているか」という中身で判断します。この考え方を構造的型付け(Structural Typing) と呼びます。
そのため、{ length: number } を継承していない自作のクラスでも、length を持っていれば条件を満たします。
class Rope {
length = 50; // どこからも継承していない
}
getLength(new Rope()); // OK!
4. 条件型(Conditional Types)
型レベルの三項演算子です。T extends U ? X : Y は「TはUに代入可能ならX、そうでなければY」という意味になります。
type IsString<T> = T extends string ? true : false;
type A = IsString<"abc">; // true
type B = IsString<42>; // false
typeとinterfaceの比較
1.表現できる型の範囲
type はあらゆる型に名前を付けられます。一方 interface は、メンバーを 1 つずつ書き並べて定義する構文です。そのため、表せるのはメンバーの宣言を書き並べられるオブジェクト型だけで、それも一定の条件を満たすものに限られます(詳しくは後述します)。
つまり、interface で書ける型はすべて type でも書けます。
interface Person {
name: string;
age: number;
}
// typeで書ける
// type Person = { name: string, age: number };
一方、type で書ける型のうち、interface でも書けるのは一部だけです。
// interface でも書ける型(オブジェクト型)
type Point = { x: number; y: number };
// interface Point { x: number; y: number }
// interface では書けない型(オブジェクト型以外)
type Id = string; // プリミティブ型の別名
type Status = "active" | "inactive"; // ユニオン型
type ElementType<T> = T extends (infer U)[] ? U : never; // Conditional Types
interfaceのメンバーとキー
-
メンバー:
interfaceの{ }に書く宣言 1 つ 1 つのこと。 - キー:プロパティやメソッドの名前の部分(name: string の name)。コールシグネチャやコンストラクトシグネチャには、そもそもキーがありません。
メンバー制約
interface で書けるのは、「メンバーがすべて静的に確定しているオブジェクト型」 です。interface のメンバーになれるのは、次の 7 種類だけです。
interface Example {
name: string; // プロパティシグネチャ
greet(msg: string): void; // メソッドシグネチャ
(x: number): string; // コールシグネチャ
new (x: number): Example; // コンストラクトシグネチャ
[key: string]: unknown; // インデックスシグネチャ
get id(): string; // アクセサ宣言(TS 4.3+)
set id(v: string);
}
下記の型はオブジェクト型ですが、静的に確定しているオブジェクト型に該当しないため、interface で書けません。
// キーが型パラメータに依存して決まるオブジェクト型
type MyPartial<T> = { [K in keyof T]?: T[K] };
type Dict<K extends string, V> = { [P in K]: V };
type Pair = [string, number]; のようなタプル型は特殊であるため、ここでは詳しく論じません。結論としては、タプル型も interface で書けません。
ただし、型パラメータを含む type がすべて interface で書けないわけではありません。型パラメータがメンバーの型に使われているだけなら、どのメンバーがあるかは確定しているので、interface でも書けます。
// type で書いた型
type Box<T> = { value: T };
type Pair<A, B> = { first: A; second: B };
type Dict<T> = { [key: string]: T };
// 同じ型を interface で書いたもの
interface BoxI<T> { value: T }
interface PairI<A, B> { first: A; second: B }
interface DictI<T> { [key: string]: T }
また Mapped Types 構文が interface のメンバーになれません。
interface Flags {
[K in "a" | "b"]: boolean; // エラー
}
interface MyPartialI<T> {
[K in keyof T]?: T[K]; // エラー
}
キー制約
プロパティ名制約
プロパティ名は識別子・文字列リテラル・数値リテラル、またはリテラル型か unique symbol 型を持つ式の計算プロパティ名でなければなりません。
// 計算プロパティ名で使う定数
const strKey = "email"; // 型は "email"(文字列リテラル型)
const numKey = 404; // 型は 404(数値リテラル型)
const idSym: unique symbol = Symbol("id"); // 型は unique symbol
interface Sample {
// 1. 識別子
name: string;
// 2. 文字列リテラル(識別子として書けない名前も使える)
"content-type": string;
// 3. 数値リテラル
200: string;
// 4. 計算プロパティ名(リテラル型を持つ式)
[strKey]: string; // 文字列リテラル型 → "email" というキーになる
[numKey]: string; // 数値リテラル型 → 404 というキーになる
// 5. 計算プロパティ名(unique symbol 型を持つ式)
[idSym]: number;
}
const sample: Sample = {
name: "Alice",
"content-type": "application/json",
200: "OK",
email: "alice@example.com",
404: "Not Found",
[idSym]: 1,
};
下記のコードがエラーになります。
// ---- エラーになる例 ----
let wideKey = "email"; // let なので型は string(リテラル型ではない)
const plainSym: symbol = Symbol(); // 型注釈で symbol に広げている(unique symbol ではない)
interface Invalid {
[wideKey]: string; // エラー: string 型の式は使えない
[plainSym]: number; // エラー: symbol 型の式は使えない
}
メソッド名の制約
メソッド名の制約は、プロパティ名とほぼ同じです。違うのは、同じ名前のメソッドを複数書くとオーバーロードになりますが、同じ名前のプロパティを複数書いたり、プロパティとメソッドを両方書いたりすると重複エラーになります。
interface Overloads {
parse(input: string): object;
parse(input: number): object; // OK: オーバーロード
}
interface Conflict1 {
value: string;
value: string; // エラー: 識別子の重複
}
interface Conflict2 {
value: string;
value(): string; // エラー: 識別子の重複
}
2.拡張関連
拡張の仕方
type の拡張は交差型(&)、interface の拡張はインターフェース拡張(extends)で実現します。
仕方が違っても、下記のコードで定義された Person と PersonI が実質的に同じ型として扱われます。
type Name = { name: string };
type Age = { age: number };
type Person = Name & Age & { gender: 'male' | 'female' };
interface Named {
name: string
};
interface Aged {
age: number
};
interface PersonI extends Named, Aged {
gender: 'male' | 'female'
}
実質的に同じだけで、完全に同じというわけではありません。コンパイラ内部の扱いが違います。
- 交差型
&:構成要素を保持したまま扱われ、プロパティの解決や代入可能性のチェックのたびに各メンバーを再帰的に調べ直す必要があります。 -
interface extends:宣言時に1つのフラットなオブジェクト型としてまとめられます。
衝突時の挙動
インターフェース拡張(extends)はプロパティの型が衝突した時にエラーを教えてくれます。一方、交差型(&)ではエラーにならず、黙って never を生みます。
interface A { value: string }
interface B extends A { value: number } // ❌ 定義時にエラー
type C = { value: string };
type D = C & { value: number };
// エラーなし(value は string & number -> neverになる)
// ただし値を作る時はエラーになる
const d: D = { value: "hello" };
// ❌ Type 'string' is not assignable to type 'never'.
3.宣言マージ
interface は、同じ名前で複数宣言すると自動的に合体します。一方、同じ名前の type を2回宣言するとエラーになります。
interface User { name: string }
interface User { age: number }
// → User は { name: string; age: number }
type Admin = { name: string };
type Admin = { age: number }; // ❌ Duplicate identifier 'Admin'.
interface のこの性質は、ライブラリやグローバルな型を後から拡張する時に使われます。
export {}; // ファイルをモジュールにする(import/export が既にあれば不要)
declare global {
interface Window {
myAppConfig: { debug: boolean };
}
}
window.myAppConfig.debug; // ✅
逆に言えば、意図せず同じ名前のinterfaceを宣言すると、知らないうちに型が合体してしまうリスクもあります。
4.暗黙のインデックスシグネチャ
type で定義したオブジェクト型は Record<string,...> に代入できます。一方、同じ形の interface は代入できません。
type T = { a: string };
interface I { a: string }
const t: T = { a: "x" };
const i: I = { a: "x" };
const r1: Record<string, string> = t; // ✅ OK
const r2: Record<string, string> = i; // ❌ Index signature for type 'string'
// is missing in type 'I'.
interface は宣言マージで後からプロパティが増える可能性があるため、TypeScript は「すべてのプロパティがstring型である」とは保証できません。そのため、代入を許可しないのです。
typeとinterfaceの使い分け
実務での判断基準
よくある2つの流派
「基本は interface、必要なときだけ type」派
TypeScript公式ハンドブックが推奨しているスタイルです(「type の機能が必要になるまでは interface を使う」)。extends による継承が自然に書けます。
「全部 type で統一」派
Reactコミュニティなどでよくあるスタイルで、書き方が1つに揃い、意図しない宣言マージも起きません。Propsの合成で & を多用する点には注意が必要です。