TypeScriptでインターフェイスを使用する方法
著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
TypeScript は、 JavaScript 言語の拡張であり、コンパイル時の型チェッカーでJavaScriptのランタイムを使用します。
TypeScriptは、コード内のオブジェクトを表す複数の方法を提供します。そのうちの1つはインターフェイスを使用しています。 TypeScriptのインターフェイスには、2つの使用シナリオがあります。クラスが実装する必要のあるメンバーなど、クラスが従う必要のあるコントラクトを作成できます。また、通常のtype宣言と同様に、アプリケーションで型を表すこともできます。 (typesの詳細については、 TypeScriptで基本型を使用する方法とTypeScript でカスタム型を作成する方法を確認してください。)
インターフェイスとタイプが同様の機能セットを共有していることに気付くかもしれません。 実際、ほとんどの場合、一方を他方に置き換えることができます。 主な違いは、インターフェイスには同じインターフェイスに対して複数の宣言があり、TypeScriptがマージするのに対し、型は1回しか宣言できないことです。 タイプを使用して、インターフェイスでは実行できないプリミティブタイプ(stringやbooleanなど)のエイリアスを作成することもできます。
TypeScriptのインターフェースは、型構造を表現するための強力な方法です。 これらを使用すると、これらの構造の使用法をタイプセーフにし、同時に文書化できるため、開発者のエクスペリエンスが直接向上します。
このチュートリアルでは、TypeScriptでインターフェイスを作成し、それらの使用方法を学び、通常のタイプとインターフェイスの違いを理解します。 さまざまなコードサンプルを試してみてください。これらは、独自のTypeScript環境、またはブラウザで直接TypeScriptを記述できるオンライン環境である TypeScriptPlaygroundで実行できます。
前提条件
このチュートリアルに従うには、次のものが必要です。
- TypeScriptプログラムを実行して、例に従うことができる環境。 これをローカルマシンに設定するには、次のものが必要になります。
- TypeScript関連のパッケージを処理する開発環境を実行するために、Nodeとnpm(または yarn )の両方がインストールされています。 このチュートリアルは、Node.jsバージョン14.3.0およびnpmバージョン6.14.5でテストされました。 macOSまたはUbuntu18.04にインストールするには、Node.jsをインストールしてmacOSにローカル開発環境を作成する方法またはPPAを使用したインストールセクションの手順に従います。 Ubuntu18.04にNode.jsをインストールするには。 これは、 Windows Subsystem for Linux(WSL)を使用している場合にも機能します。
- さらに、TypeScriptコンパイラ(
tsc)がマシンにインストールされている必要があります。 これを行うには、公式TypeScriptWebサイトを参照してください。
- ローカルマシン上にTypeScript環境を作成したくない場合は、公式の TypeScriptPlaygroundを使用してフォローできます。
- JavaScript、特に destructuring、REST演算子、 imports /exportsなどのES6+構文に関する十分な知識が必要です。 これらのトピックに関する詳細情報が必要な場合は、JavaScriptシリーズのコーディング方法を読むことをお勧めします。
- このチュートリアルでは、TypeScriptをサポートし、インラインエラーを表示するテキストエディタの側面を参照します。 これはTypeScriptを使用するために必要ではありませんが、TypeScript機能をさらに活用します。 これらの利点を活用するには、 Visual Studio Code のようなテキストエディターを使用できます。このエディターは、そのままTypeScriptを完全にサポートしています。 TypeScriptPlaygroundでこれらの利点を試すこともできます。
このチュートリアルに示されているすべての例は、TypeScriptバージョン4.2.2を使用して作成されています。
TypeScriptでのインターフェイスの作成と使用
このセクションでは、TypeScriptで利用可能なさまざまな機能を使用してインターフェイスを作成します。 また、作成したインターフェースの使用方法についても学習します。
TypeScriptのインターフェイスは、interfaceキーワード、インターフェイスの名前、インターフェイスの本体を含む{}ブロックを使用して作成されます。 たとえば、Loggerインターフェイスは次のとおりです。
interface Logger {
log: (message: string) => void;
}
type宣言を使用して通常の型を作成するのと同様に、{}で型のフィールドとその型を指定します。
interface Logger {
log: (message: string) => void;
}
Loggerインターフェースは、logと呼ばれる単一のプロパティを持つオブジェクトを表します。 このプロパティは、タイプstringの単一のパラメーターを受け取り、voidを返す関数です。
Loggerインターフェースは他のタイプと同じように使用できます。 Loggerインターフェイスに一致するオブジェクトリテラルを作成する例を次に示します。
interface Logger {
log: (message: string) => void;
}
const logger: Logger = {
log: (message) => console.log(message),
};
タイプとしてLoggerインターフェースを使用する値は、Loggerインターフェース宣言で指定されたものと同じメンバーを持っている必要があります。 一部のメンバーがオプションの場合、それらは省略できます。
値はインターフェイスで宣言されているものに従う必要があるため、無関係なフィールドを追加するとコンパイルエラーが発生します。 たとえば、オブジェクトリテラルで、インターフェイスにない新しいプロパティを追加してみてください。
interface Logger {
log: (message: string) => void;
}
const logger: Logger = {
log: (message) => console.log(message),
otherProp: true,
};
この場合、TypeScriptコンパイラはエラー2322を発行します。これは、このプロパティがLoggerインターフェイス宣言に存在しないためです。
OutputType '{ log: (message: string) => void; otherProp: boolean; }' is not assignable to type 'Logger'.
Object literal may only specify known properties, and 'otherProp' does not exist in type 'Logger'. (2322)
通常のtype宣言を使用するのと同様に、プロパティは、名前に?を追加することにより、オプションのプロパティに変えることができます。
他のタイプの拡張
インターフェイスを作成するときに、さまざまなオブジェクトタイプから拡張して、拡張タイプのすべてのタイプ情報をインターフェイスに含めることができます。 これにより、共通のフィールドセットを使用して小さなインターフェイスを記述し、それらをビルディングブロックとして使用して新しいインターフェイスを作成できます。
次のようなClearableインターフェイスがあるとします。
interface Clearable {
clear: () => void;
}
次に、そのすべてのフィールドを継承して、そこから拡張する新しいインターフェイスを作成できます。 次の例では、インターフェイスLoggerがClearableインターフェイスから拡張されています。 強調表示された行に注意してください。
interface Clearable {
clear: () => void;
}
interface Logger extends Clearable {
log: (message: string) => void;
}
Loggerインターフェースには、clearメンバーも含まれるようになりました。これは、パラメーターを受け入れず、voidを返す関数です。 この新しいメンバーは、Clearableインターフェースから継承されます。 これを行った場合と同じです。
interface Logger {
log: (message: string) => void;
clear: () => void;
}
共通のフィールドセットを使用して多数のインターフェイスを作成する場合、それらを別のインターフェイスに抽出し、作成した新しいインターフェイスから拡張するようにインターフェイスを変更できます。
前に使用したClearableの例に戻り、アプリケーションが複数の文字列を保持するデータ構造を表すために、次のStringListインターフェイスなどの別のインターフェイスを必要とすることを想像してください。
interface StringList {
push: (value: string) => void;
get: () => string[];
}
この新しいStringListインターフェイスで既存のClearableインターフェイスを拡張することにより、このインターフェイスにもClearableインターフェイスで設定されたメンバーがあり、clearが追加されることを指定します。 ]プロパティをStringListインターフェイスの型定義に追加します。
interface StringList extends Clearable {
push: (value: string) => void;
get: () => string[];
}
インターフェイスは、インターフェイス、通常のタイプ、さらにはクラスなどの任意のオブジェクトタイプから拡張できます。
呼び出し可能な署名を持つインターフェース
インターフェイスも呼び出し可能である場合(つまり、関数でもある場合)、呼び出し可能シグニチャを作成することにより、インターフェイス宣言でその情報を伝えることができます。
呼び出し可能な署名は、どのメンバーにもバインドされていないインターフェイス内に関数宣言を追加し、関数の戻り型を設定するときに=>の代わりに:を使用することによって作成されます。
例として、以下の強調表示されたコードのように、呼び出し可能な署名をLoggerインターフェースに追加します。
interface Logger {
(message: string): void;
log: (message: string) => void;
}
呼び出し可能なシグネチャは無名関数の型宣言に似ていますが、戻り型では=>の代わりに:を使用していることに注意してください。 これは、Loggerインターフェイスタイプにバインドされた任意の値を関数として直接呼び出すことができることを意味します。
Loggerインターフェースに一致する値を作成するには、インターフェースの要件を考慮する必要があります。
- 呼び出し可能でなければなりません。
- 単一の
stringパラメーターを受け入れる関数であるlogというプロパティが必要です。
Loggerインターフェースのタイプに割り当て可能なloggerという変数を作成しましょう。
interface Logger {
(message: string): void;
log: (message: string) => void;
}
const logger: Logger = (message: string) => {
console.log(message);
}
logger.log = (message: string) => {
console.log(message);
}
Loggerインターフェースと一致させるには、値が呼び出し可能である必要があります。そのため、logger変数を関数に割り当てます。
interface Logger {
(message: string): void;
log: (message: string) => void;
}
const logger: Logger = (message: string) => {
console.log(message);
}
logger.log = (message: string) => {
console.log(message);
}
次に、logプロパティをlogger関数に追加します。
interface Logger {
(message: string): void;
log: (message: string) => void;
}
const logger: Logger = (message: string) => {
console.log(message);
}
logger.log = (message: string) => {
console.log(message);
}
これは、Loggerインターフェースで必要です。 Loggerインターフェイスにバインドされた値には、単一のstringパラメーターを受け入れる関数であり、voidを返すlogプロパティも必要です。
logプロパティを含めなかった場合、TypeScriptコンパイラはエラー2741を返します。
OutputProperty 'log' is missing in type '(message: string) => void' but required in type 'Logger'. (2741)
logger変数のlogプロパティに、trueに設定するなど、互換性のない型シグネチャがある場合、TypeScriptコンパイラは同様のエラーを発行します。
interface Logger {
(message: string): void;
log: (message: string) => void;
}
const logger: Logger = (message: string) => {
console.log(message);
}
logger.log = true;
この場合、TypeScriptコンパイラはエラー2322を表示します。
OutputType 'boolean' is not assignable to type '(message: string) => void'. (2322)
変数を特定のタイプに設定することの優れた機能、この場合はlogger変数をLoggerインターフェースのタイプに設定することで、TypeScriptがパラメーターのタイプを推測できるようになりました。 logger関数とlogプロパティの関数の両方。
両方の関数の引数から型情報を削除することで確認できます。 以下の強調表示されたコードでは、messageパラメーターにタイプがないことに注意してください。
interface Logger {
(message: string): void;
log: (message: string) => void;
}
const logger: Logger = (message) => {
console.log(message);
}
logger.log = (message) => {
console.log(message);
}
また、どちらの場合も、パラメーターのタイプがstringであることをエディターが表示できるはずです。これは、Loggerインターフェースで予期されるタイプであるためです。
インデックス署名付きのインターフェイス
通常のタイプの場合と同じように、インターフェイスにインデックスシグネチャを追加できるため、インターフェイスに無制限の数のプロパティを設定できます。
たとえば、stringフィールドの数に制限がないDataRecordインターフェイスを作成する場合は、次の強調表示されたインデックス署名を使用できます。
interface DataRecord {
[key: string]: string;
}
次に、DataRecordインターフェイスを使用して、タイプstringの複数のパラメーターを持つオブジェクトのタイプを設定できます。
interface DataRecord {
[key: string]: string;
}
const data: DataRecord = {
fieldA: "valueA",
fieldB: "valueB",
fieldC: "valueC",
// ...
};
このセクションでは、TypeScriptで利用可能なさまざまな機能を使用してインターフェイスを作成し、作成したインターフェイスの使用方法を学習しました。 次のセクションでは、type宣言とinterface宣言の違いについて詳しく学び、宣言のマージとモジュールの拡張について練習します。
タイプとインターフェースの違い
これまでのところ、interface宣言とtype宣言は類似しており、ほぼ同じ機能セットを備えています。
たとえば、Clearableインターフェイスから拡張されたLoggerインターフェイスを作成しました。
interface Clearable {
clear: () => void;
}
interface Logger extends Clearable {
log: (message: string) => void;
}
同じタイプの表現は、2つのtype宣言を使用して複製できます。
type Clearable = {
clear: () => void;
}
type Logger = Clearable & {
log: (message: string) => void;
}
前のセクションで示したように、interface宣言は、関数から無制限の数のプロパティを持つ複雑なオブジェクトまで、さまざまなオブジェクトを表すために使用できます。 これは、type宣言でも可能です。交差演算子&を使用して複数のタイプを交差させることができるため、他のタイプから拡張することもできます。
type宣言とinterface宣言は非常に似ているため、それぞれに固有の特定の機能を検討し、コードベースで一貫性を保つ必要があります。 1つを選択してコードベースに型表現を作成し、もう1つは、その機能でのみ使用できる特定の機能が必要な場合にのみ使用します。
たとえば、type宣言には、interface宣言にはない機能がいくつかあります。
- 共用体タイプ。
- マップされたタイプ。
- プリミティブ型のエイリアス。
interface宣言でのみ使用できる機能の1つは、宣言のマージです。これについては、次のセクションで学習します。 type宣言では不可能なため、ライブラリを作成していて、ライブラリユーザーがライブラリによって提供される型を拡張できるようにしたい場合は、宣言のマージが役立つ場合があることに注意してください。
宣言のマージ
TypeScriptは、複数の宣言を1つの宣言にマージできるため、同じデータ構造に対して複数の宣言を記述し、コンパイル中にTypeScriptコンパイラによってそれらを単一の型であるかのようにバンドルすることができます。 このセクションでは、これがどのように機能するか、およびインターフェイスを使用するときになぜ役立つのかを説明します。
TypeScriptのインターフェイスを再度開くことができます。 つまり、同じインターフェイスの複数の宣言をマージできます。 これは、既存のインターフェイスに新しいフィールドを追加する場合に役立ちます。
たとえば、次のようなDatabaseOptionsという名前のインターフェイスがあるとします。
interface DatabaseOptions {
host: string;
port: number;
user: string;
password: string;
}
このインターフェースは、データベースに接続するときにオプションを渡すために使用されます。
コードの後半で、次のように、同じ名前でdsnUrlという単一のstringフィールドを持つインターフェイスを宣言します。
interface DatabaseOptions {
dsnUrl: string;
}
TypeScriptコンパイラがコードの読み取りを開始すると、DatabaseOptionsインターフェイスのすべての宣言が1つにマージされます。 TypeScriptコンパイラの観点から、DatabaseOptionsは次のようになります。
interface DatabaseOptions {
host: string;
port: number;
user: string;
password: string;
dsnUrl: string;
}
インターフェイスには、最初に宣言したすべてのフィールドに加えて、個別に宣言した新しいフィールドdsnUrlが含まれます。 両方の宣言がマージされました。
モジュールの拡張
宣言のマージは、既存のモジュールを新しいプロパティで拡張する必要がある場合に役立ちます。 そのユースケースの1つは、ライブラリによって提供されるデータ構造にフィールドを追加する場合です。 これは、expressと呼ばれるNode.jsライブラリで比較的一般的であり、HTTPサーバーを作成できます。
expressを操作する場合、RequestおよびResponseオブジェクトがリクエストハンドラー(HTTPリクエストへの応答を提供する機能)に渡されます。 Requestオブジェクトは通常、特定のリクエストに固有のデータを保存するために使用されます。 たとえば、これを使用して、最初のHTTPリクエストを行ったログに記録されたユーザーを保存できます。
const myRoute = (req: Request, res: Response) => {
res.json({ user: req.user });
}
ここで、要求ハンドラーは、userフィールドがログに記録されたユーザーに設定されたjsonをクライアントに送り返します。 ログに記録されたユーザーは、ユーザー認証を担当するエクスプレスミドルウェアを使用して、コード内の別の場所でリクエストオブジェクトに追加されます。
Requestインターフェイス自体の型定義にはuserフィールドがないため、上記のコードでは型エラー2339が発生します。
Property 'user' does not exist on type 'Request'. (2339)
これを修正するには、expressパッケージのモジュール拡張を作成し、宣言のマージを利用してRequestインターフェイスに新しいプロパティを追加する必要があります。
express型宣言でRequestオブジェクトの型を確認すると、ドキュメントに示されているように、Expressというグローバル名前空間内に追加されたインターフェイスであることがわかります。 DefinitelyTypedリポジトリから:
declare global {
namespace Express {
// These open interfaces may be extended in an application-specific manner via declaration merging.
// See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
interface Request {}
interface Response {}
interface Application {}
}
}
注:型宣言ファイルは、型情報のみを含むファイルです。 DefinitelyTypedリポジトリは、型宣言がないパッケージの型宣言を送信するための公式リポジトリです。 npmで利用可能な@types/<package>パッケージは、このリポジトリから公開されています。
モジュール拡張を使用してRequestインターフェイスに新しいプロパティを追加するには、ローカルタイプ宣言ファイルで同じ構造を複製する必要があります。 たとえば、次のようなexpress.d.tsという名前のファイルを作成し、それをtsconfig.jsonのtypesオプションに追加したとします。
import 'express';
declare global {
namespace Express {
interface Request {
user: {
name: string;
}
}
}
}
TypeScriptコンパイラの観点からは、Requestインターフェイスにはuserプロパティがあり、そのタイプはnameというタイプの[という単一のプロパティを持つオブジェクトに設定されています。 X181X]。 これは、同じインターフェイスのすべての宣言がマージされるために発生します。
ライブラリを作成していて、上記のexpressで行ったように、ライブラリのユーザーに独自のライブラリによって提供されるタイプを拡張するオプションを提供したいとします。 その場合、通常のtype宣言はモジュール拡張をサポートしないため、ライブラリからインターフェイスをエクスポートする必要があります。
結論
このチュートリアルでは、さまざまなデータ構造を表す複数のTypeScriptインターフェイスを作成し、さまざまなインターフェイスをビルディングブロックとして一緒に使用して強力な型を作成する方法を発見し、通常の型宣言とインターフェイスの違いについて学習しました。 これで、コードベース内のデータ構造のインターフェイスの記述を開始できるようになり、ドキュメントだけでなくタイプセーフなコードも使用できるようになりました。
TypeScriptのその他のチュートリアルについては、TypeScriptシリーズのコーディング方法ページをご覧ください。