このページは廃止されました

このハンドブックのページは置き換えられました。新しいページに移動

インターフェース

TypeScriptの核となる原則の1つは、型チェックが値の形状に焦点を合わせていることです。これは、「ダックタイピング」または「構造的部分型」と呼ばれることもあります。TypeScriptでは、インターフェースはこれらの型に名前を付ける役割を果たし、コード内およびプロジェクト外のコードとのコントラクトを定義するための強力な方法です。

最初のインターフェース

インターフェースの仕組みを理解する最も簡単な方法は、簡単な例から始めることです。

ts
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
 
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Try

型チェッカーは`printLabel`の呼び出しをチェックします。`printLabel`関数は、渡されたオブジェクトに`string`型の`label`というプロパティがあることを要求する単一のパラメーターを持ちます。実際には、オブジェクトはこのパラメーターよりも多くのプロパティを持っていますが、コンパイラーは、少なくとも必要なプロパティが存在し、必要な型と一致しているかどうかのみをチェックすることに注意してください。TypeScriptがそれほど寛容でない場合もありますが、それについては後で説明します。

同じ例をもう一度書くことができます。今回は、`string`型の`label`プロパティを持つという要件を記述するためにインターフェースを使用します。

ts
interface LabeledValue {
label: string;
}
 
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
 
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Try

インターフェース`LabeledValue`は、前の例の要件を記述するために使用できる名前です。これは、`string`型である`label`という単一のプロパティを持つことを表しています。他の言語のように、`printLabel`に渡すオブジェクトがこのインターフェースを実装していると明示的に言う必要がなかったことに注意してください。ここでは、形状だけが重要です。関数に渡すオブジェクトがリストされている要件を満たしていれば、許可されます。

型チェッカーは、これらのプロパティが何らかの順序で提供されることを要求せず、インターフェースが必要とするプロパティが存在し、必要な型を持っていることのみを要求することに注意してください。

オプションのプロパティ

インターフェースのすべてのプロパティが必須であるわけではありません。特定の条件下でのみ存在するものや、まったく存在しないものもあります。これらのオプションのプロパティは、「オプションバッグ」のようなパターンを作成する場合に役立ちます。このパターンでは、いくつかのプロパティのみが入力されたオブジェクトを関数に渡します。

このパターンの例を次に示します。

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({ color: "black" });
Try

オプションのプロパティを持つインターフェースは、他のインターフェースと同様に記述されます。各オプションのプロパティは、宣言のプロパティ名の末尾に`?`を付けて表します。

オプションのプロパティの利点は、これらの潜在的に使用可能なプロパティを記述しながら、インターフェースの一部ではないプロパティの使用を防ぐことができることです。たとえば、`createSquare`で`color`プロパティの名前を間違えて入力した場合、それを知らせるエラーメッセージが表示されます。

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.clor) {
Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({ color: "black" });
Try

読み取り専用プロパティ

オブジェクトが最初に作成されたときのみ変更可能なプロパティがあります。プロパティ名の前にreadonlyを付けることで、これを指定できます。

ts
interface Point {
readonly x: number;
readonly y: number;
}
Try

オブジェクトリテラルを代入することで、Pointを構築できます。代入後、xyは変更できません。

ts
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
Cannot assign to 'x' because it is a read-only property.2540Cannot assign to 'x' because it is a read-only property.
Try

TypeScriptには、すべての変更メソッドが削除されたArray<T>と同じであるReadonlyArray<T>型が付属しているため、作成後に配列が変更されないようにすることができます。

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
 
ro[0] = 12; // error!
Index signature in type 'readonly number[]' only permits reading.2542Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
Property 'push' does not exist on type 'readonly number[]'.2339Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
Cannot assign to 'length' because it is a read-only property.2540Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.4104The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Try

スニペットの最後の行では、ReadonlyArray全体を通常の配列に代入することさえできないことがわかります。ただし、型アサーションでオーバーライドすることはできます。

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
 
a = ro as number[];
Try

readonly vs const

readonlyまたはconstを使用するかどうかを覚える最も簡単な方法は、変数またはプロパティのどちらで使用しているかを確認することです。変数はconstを使用し、プロパティはreadonlyを使用します。

過剰プロパティチェック

インターフェースを使用した最初の例では、TypeScriptでは、{ label: string; }のみを想定しているものに{ size: number; label: string; }を渡すことができます。また、オプションのプロパティと、いわゆる「オプションバッグ」を記述する際にどのように役立つかについても学びました。

ただし、2つを単純に組み合わせると、エラーが忍び込む可能性があります。たとえば、createSquareを使用した最後の例を見てみましょう。

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
 
let mySquare = createSquare({ colour: "red", width: 100 });
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

createSquareに渡された引数が、colorではなくcolourと綴られていることに注意してください。プレーンJavaScriptでは、この種のことは静かに失敗します。

widthプロパティは互換性があり、colorプロパティが存在せず、追加のcolourプロパティは重要ではないため、このプログラムは正しく型指定されていると主張することもできます。

ただし、TypeScriptはこのコードにバグがある可能性があると判断します。オブジェクトリテラルは特別な扱いをされ、他の変数に代入したり、引数として渡したりするときに、*過剰プロパティチェック*が行われます。オブジェクトリテラルに「ターゲット型」にないプロパティがある場合、エラーが発生します。

ts
let mySquare = createSquare({ colour: "red", width: 100 });
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

これらのチェックを回避するのは実際には非常に簡単です。最も簡単な方法は、型アサーションを使用することです。

ts
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
Try

ただし、オブジェクトが特別な方法で使用される追加のプロパティを持つことができることが確実な場合は、文字列インデックスシグネチャを追加する方が良い方法かもしれません。SquareConfigが上記の型を持つcolorおよびwidthプロパティを持つことができ、*さらに*他のプロパティをいくつでも持つことができる場合は、次のように定義できます。

ts
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
Try

インデックスシグネチャについては後で説明しますが、ここでは、SquareConfigは任意の数のプロパティを持つことができ、それらがcolorまたはwidthでない限り、それらの型は重要ではないと言っています。

これらのチェックを回避する最後の方法は、少し驚くかもしれませんが、オブジェクトを別の変数に代入することです。squareOptionsは過剰プロパティチェックを受けないため、コンパイラはエラーを出しません。

ts
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Try

上記の回避策は、squareOptionsSquareConfigの間に共通のプロパティがある限り機能します。この例では、それはプロパティwidthでした。ただし、変数に共通のオブジェクトプロパティがない場合は失敗します。例えば

ts
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.
Try

上記のような単純なコードの場合、これらのチェックを「回避」しようとするべきではないことに注意してください。メソッドを持ち、状態を保持するより複雑なオブジェクトリテラルの場合、これらの手法を覚えておく必要があるかもしれませんが、過剰プロパティエラーの大部分は実際にはバグです。つまり、オプションバッグのようなものの過剰プロパティチェックの問題が発生している場合は、型宣言の一部を修正する必要があるかもしれません。このインスタンスでは、colorまたはcolourプロパティの両方を持つオブジェクトをcreateSquareに渡すことが問題ない場合は、SquareConfigの定義を修正してそれを反映する必要があります。

関数型

インターフェースは、JavaScriptオブジェクトがとることができる幅広い形状を記述できます。プロパティを持つオブジェクトを記述することに加えて、インターフェースは関数型を記述することもできます。

インターフェースで関数型を記述するには、インターフェースに呼び出しシグネチャを指定します。これは、パラメータリストと戻り値の型のみが指定された関数宣言のようなものです。パラメータリストの各パラメータには、名前と型の両方が必要です。

ts
interface SearchFunc {
(source: string, subString: string): boolean;
}
Try

定義したら、この関数型インターフェースを他のインターフェースと同様に使用できます。ここでは、関数型の変数を作成し、それに同じ型の関数値を代入する方法を示します。

ts
let mySearch: SearchFunc;
 
mySearch = function (source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1;
};
Try

関数型が正しく型チェックされるためには、パラメータの名前が一致する必要はありません。たとえば、上記の例は次のように書くこともできました。

ts
let mySearch: SearchFunc;
 
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};
Try

関数パラメータは一度に1つずつチェックされ、対応する各パラメータ位置の型が互いにチェックされます。型をまったく指定したくない場合は、関数値が型SearchFuncの変数に直接代入されるため、TypeScriptのコンテキスト型指定で引数の型を推論できます。ここでも、関数式の戻り値の型は、それが返す値(ここではfalsetrue)によって暗示されます。

ts
let mySearch: SearchFunc;
 
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
};
Try

関数式が数値または文字列を返した場合、型チェッカーは、戻り値の型がSearchFuncインターフェースで記述されている戻り値の型と一致しないことを示すエラーを生成します。

ts
let mySearch: SearchFunc;
 
mySearch = function (src, sub) {
Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.2322Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.
let result = src.search(sub);
return "string";
};
Try

インデックス可能な型

インターフェースを使用して関数型を記述する方法と同様に、a[10]ageMap["daniel"]のように「インデックス化できる」型を記述することもできます。インデックス可能な型には、オブジェクトにインデックスを付けるために使用できる型と、インデックス付け時の対応する戻り値の型を記述する*インデックスシグネチャ*があります。

例を見てみましょう。

ts
interface StringArray {
[index: number]: string;
}
 
let myArray: StringArray;
myArray = ["Bob", "Fred"];
 
let myStr: string = myArray[0];
Try

上記では、インデックスシグネチャを持つStringArrayインターフェースがあります。このインデックスシグネチャは、StringArraynumberでインデックス付けされると、stringを返すことを示しています。

サポートされているインデックスシグネチャには、文字列、数値、シンボル、テンプレート文字列の4種類があります。多くのタイプのインデクサーをサポートすることは可能ですが、数値インデクサーから返される型は、文字列インデクサーから返される型のサブタイプである必要があります。

これは、numberでインデックスを作成する場合、JavaScriptはオブジェクトにインデックスを作成する前に実際にそれをstringに変換するためです。つまり、100number)でインデックスを作成することは、"100"string)でインデックスを作成することと同じであるため、2つは一致する必要があります。

ts
interface Animal {
name: string;
}
 
interface Dog extends Animal {
breed: string;
}
 
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.2413'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
Try

文字列インデックスシグネチャは「辞書」パターンを記述するための強力な方法ですが、すべてのプロパティが戻り値の型と一致することも強制します。これは、文字列インデックスがobj.propertyobj["property"]としても使用可能であると宣言するためです。次の例では、nameの型は文字列インデックスの型と一致しないため、型チェッカーはエラーを返します。

ts
interface NumberDictionary {
[index: string]: number;
 
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.2411Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
Try

ただし、インデックスシグネチャがプロパティ型の共用体である場合、異なる型のプロパティは許容されます。

ts
interface NumberOrStringDictionary {
[index: string]: number | string;
 
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Try

最後に、インデックスへの代入を防ぐために、インデックスシグネチャをreadonlyにすることができます。

ts
interface ReadonlyStringArray {
readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.
Try

インデックスシグネチャがreadonlyであるため、myArray[2]を設定できません。

テンプレート文字列を使用したインデックス可能な型

テンプレート文字列を使用して、特定のパターンが許可されるが、すべてではないことを示すことができます。たとえば、HTTPヘッダーオブジェクトには、既知のヘッダーのセットリストがあり、`x-`で始まるカスタム定義プロパティをサポートできます。

ts
interface HeadersResponse {
"content-type": string,
date: string,
"content-length": string
 
// Permit any property starting with 'x-'.
[headerName: `x-${string}`]: string;
}
 
function handleResponse(r: HeadersResponse) {
// Handle known, and x- prefixed
const type = r["content-type"]
const poweredBy = r["x-powered-by"]
 
// Unknown keys without the prefix raise errors
const origin = r.origin
Property 'origin' does not exist on type 'HeadersResponse'.2339Property 'origin' does not exist on type 'HeadersResponse'.
}
Try

クラス型

インターフェースの実装

C#やJavaなどの言語でインターフェースの最も一般的な用途の1つである、クラスが特定の契約を満たすことを明示的に強制することも、TypeScriptで可能です。

ts
interface ClockInterface {
currentTime: Date;
}
 
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) {}
}
Try

以下の例で`setTime`を使用しているように、クラスで実装されているインターフェースでメソッドを記述することもできます。

ts
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
 
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
Try

インターフェースは、パブリックとプライベートの両方ではなく、クラスのパブリック側を記述します。これにより、クラスインスタンスのプライベート側にも特定の型があることを確認するために使用することができなくなります。

~クラスの静的側とインスタンス側の違い

クラスとインターフェースを扱う場合、クラスには*2つ*の型があることに注意してください。静的側の型とインスタンス側の型です。コンストラクトシグネチャを持つインターフェースを作成し、このインターフェースを実装するクラスを作成しようとすると、エラーが発生することに気付くかもしれません。

ts
interface ClockConstructor {
new (hour: number, minute: number);
}
 
class Clock implements ClockConstructor {
Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.2420Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
currentTime: Date;
constructor(h: number, m: number) {}
}
Try

これは、クラスがインターフェースを実装する場合、クラスのインスタンス側のみがチェックされるためです。コンストラクターは静的側に存在するため、このチェックには含まれません。

代わりに、クラスの静的側で直接作業する必要があります。この例では、コンストラクター用の`ClockConstructor`とインスタンスメソッド用の`ClockInterface`の2つのインターフェースを定義します。次に、便宜上、渡された型のインスタンスを作成するコンストラクター関数`createClock`を定義します。

ts
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
 
interface ClockInterface {
tick(): void;
}
 
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
 
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}
 
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("tick tock");
}
}
 
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Try

`createClock`の最初のパラメーターは`ClockConstructor`型であるため、`createClock(AnalogClock, 7, 32)`では、`AnalogClock`に正しいコンストラクターシグネチャがあることをチェックします。

もう1つの簡単な方法は、クラス式を使用することです。

ts
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
 
interface ClockInterface {
tick(): void;
}
 
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};
 
let clock = new Clock(12, 17);
clock.tick();
Try

~インターフェースの拡張

クラスと同様に、インターフェースは互いに拡張できます。これにより、1つのインターフェースのメンバーを別のインターフェースにコピーできるため、インターフェースを再利用可能なコンポーネントに分割する方法に柔軟性が生まれます。

ts
interface Shape {
color: string;
}
 
interface Square extends Shape {
sideLength: number;
}
 
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
Try

インターフェースは複数のインターフェースを拡張して、すべてのインターフェースの組み合わせを作成できます。

ts
interface Shape {
color: string;
}
 
interface PenStroke {
penWidth: number;
}
 
interface Square extends Shape, PenStroke {
sideLength: number;
}
 
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Try

~ハイブリッド型

前述のように、インターフェースは現実世界のJavaScriptに存在する豊富な型を記述できます。JavaScriptの動的で柔軟な性質のため、上記の型の組み合わせとして機能するオブジェクトに遭遇することがあります。

そのような例の1つは、関数とオブジェクトの両方として機能し、追加のプロパティを持つオブジェクトです。

ts
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
 
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
 
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Try

サードパーティのJavaScriptと対話する場合、上記のパータンを使用して型の形状を完全に記述する必要がある場合があります。

~クラスを拡張するインターフェース

インターフェース型がクラス型を拡張する場合、クラスのメンバーは継承されますが、実装は継承されません。インターフェースが実装を提供せずにクラスのすべてのメンバーを宣言したかのようです。インターフェースは、基底クラスのプライベートメンバーと保護されたメンバーも継承します。つまり、プライベートメンバーまたは保護されたメンバーを持つクラスを拡張するインターフェースを作成する場合、そのインターフェース型は、そのクラスまたはそのサブクラスによってのみ実装できます。

これは、大きな継承階層があるが、コードが特定のプロパティを持つサブクラスのみで動作するように指定する場合に便利です。サブクラスは、基底クラスから継承する以外に関連付ける必要はありません。次に例を示します。

ts
class Control {
private state: any;
}
 
interface SelectableControl extends Control {
select(): void;
}
 
class Button extends Control implements SelectableControl {
select() {}
}
 
class TextBox extends Control {
select() {}
}
 
class ImageControl implements SelectableControl {
Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.2420Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}
Try

上記の例では、`SelectableControl`には、プライベート`state`プロパティを含む`Control`のすべてのメンバーが含まれています。 `state`はプライベートメンバーであるため、`Control`の子孫のみが`SelectableControl`を実装できます。これは、`Control`の子孫のみが、同じ宣言で発生する`state`プライベートメンバーを持つためです。これは、プライベートメンバーが互換性を持つための要件です。

`Control`クラス内では、`SelectableControl`のインスタンスを介して`state`プライベートメンバーにアクセスできます。事実上、`SelectableControl`は、`select`メソッドを持つことがわかっている`Control`のように動作します。 `Button`クラスと`TextBox`クラスは`SelectableControl`のサブタイプです(どちらも`Control`から継承し、`select`メソッドを持っているため)。 `ImageControl`クラスには、`Control`を拡張するのではなく独自の`state`プライベートメンバーがあるため、`SelectableControl`を実装できません。

TypeScriptドキュメントはオープンソースプロジェクトです。❤プルリクエストを送信して、これらのページの改善にご協力ください。

このページの投稿者
RCRyan Cavanaugh (55)
DRDaniel Rosenwasser (25)
OTOrta Therox (23)
Jjswheeler (3)
MHMohamed Hegazy (3)
44+

最終更新日:2024年3月21日