著者は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シリーズのコーディング方法ページをご覧ください。