TypeScriptで名前空間を使用する方法
著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
TypeScript は、 JavaScript 言語の拡張であり、コンパイル時の型チェッカーでJavaScriptのランタイムを使用します。 TypeScriptでは、名前空間を使用してコードを整理できます。 以前は内部モジュールと呼ばれていたTypeScriptの名前空間は、ECMAScriptモジュールの初期ドラフトに基づいています。 ECMAScript仕様ドラフトでは、内部モジュールは 2013年9月頃に削除されましたが、TypeScriptはそのアイデアを別の名前で保持していました。
名前空間を使用すると、開発者は、プロパティ、クラス、タイプ、インターフェイスなどの複数の値を保持するために使用できる個別の組織単位を作成できます。 このチュートリアルでは、名前空間を作成して使用し、構文とその使用目的を説明します。 名前空間の宣言とマージのコードサンプル、名前空間が内部でJavaScriptコードとしてどのように機能するか、および入力せずに外部ライブラリの型を宣言するためにそれらを使用する方法について説明します。
前提条件
このチュートリアルに従うには、次のものが必要です。
- TypeScriptプログラムを実行して、例に従うことができる環境。 これをローカルマシンに設定するには、次のものが必要です。
TypeScript関連のパッケージを処理する開発環境を実行するためにインストールされたNodeとnpm(またはyarn)の両方。 このチュートリアルは、Node.jsバージョン14.3.0およびnpmバージョン6.14.5でテストされました。 macOSまたはUbuntu18.04にインストールするには、「Node.jsをインストールしてmacOSにローカル開発環境を作成する方法」または「Ubuntu18.04にNode.jsをインストールする方法」の「PPAを使用したインストール」セクションの手順に従います。 これは、Windows Subsystem for Linux(WSL)を使用している場合にも機能します。 さらに、TypeScriptコンパイラ(tsc)がマシンにインストールされている必要があります。 これを行うには、TypeScriptの公式Webサイトを参照してください。 - ローカルマシン上にTypeScript環境を作成したくない場合は、公式の TypeScriptPlaygroundを使用してフォローできます。
- JavaScript、特に destructuring、rest演算子、 imports /exportsなどのES6+構文に関する十分な知識が必要です。 これらのトピックに関する詳細情報が必要な場合は、JavaScriptシリーズのコーディング方法を読むことをお勧めします。
- このチュートリアルでは、TypeScriptをサポートし、インラインエラーを表示するテキストエディタの側面を参照します。 これはTypeScriptを使用するために必要ではありませんが、TypeScript機能をさらに活用します。 これらの利点を活用するには、 Visual Studio Code のようなテキストエディターを使用できます。このエディターは、TypeScriptをすぐにサポートします。 TypeScriptPlaygroundでこれらの利点を試すこともできます。
このチュートリアルに示されているすべての例は、TypeScriptバージョン4.2.3を使用して作成されています。
TypeScriptで名前空間を作成する
このセクションでは、一般的な構文を説明するために、TypeScriptで名前空間を作成します。
名前空間を作成するには、namespace
キーワード、名前空間の名前、{}
ブロックの順に使用します。 例として、オブジェクトリレーショナルマッピング(ORM)ライブラリを使用しているかのように、データベースエンティティを保持するDatabaseEntity
名前空間を作成します。 次のコードを新しいTypeScriptファイルに追加します。
namespace DatabaseEntity {
}
これはDatabaseEntity
名前空間を宣言しますが、その名前空間にコードをまだ追加していません。 次に、名前空間内にUser
クラスを追加して、データベース内のUser
エンティティを表します。
namespace DatabaseEntity {
class User {
constructor(public name: string) {}
}
}
User
クラスは、通常、名前空間内で使用できます。 これを説明するために、新しいUser
インスタンスを作成し、それをnewUser
変数に格納します。
namespace DatabaseEntity {
class User {
constructor(public name: string) {}
}
const newUser = new User("Jon");
}
これは有効なコードです。 ただし、名前空間の外でUser
を使用しようとすると、TypeScriptコンパイラでエラー2339
が発生します。
OutputProperty 'User' does not exist on type 'typeof DatabaseEntity'. (2339)
名前空間の外部でクラスを使用する場合は、以下の強調表示されたコードに示すように、最初にUser
クラスをエクスポートして外部で使用できるようにする必要があります。
namespace DatabaseEntity {
export class User {
constructor(public name: string) {}
}
const newUser = new User("Jon");
}
これで、完全修飾名を使用して、DatabaseEntity
名前空間の外部のUser
クラスにアクセスできるようになりました。 この場合、完全修飾名はDatabaseEntity.User
です。
namespace DatabaseEntity {
export class User {
constructor(public name: string) {}
}
const newUser = new User("Jon");
}
const newUserOutsideNamespace = new DatabaseEntity.User("Jane");
変数を含め、名前空間内から何でもエクスポートできます。変数は、名前空間のプロパティになります。 次のコードでは、newUser
変数をエクスポートしています。
namespace DatabaseEntity {
export class User {
constructor(public name: string) {}
}
export const newUser = new User("Jon");
}
console.log(DatabaseEntity.newUser.name);
変数newUser
がエクスポートされたため、名前空間のプロパティとしてアクセスできます。 このコードを実行すると、コンソールに次のように出力されます。
OutputJon
interfaces と同様に、TypeScriptの名前空間でも宣言のマージが可能です。 これは、同じ名前空間の複数の宣言が1つの宣言にマージされることを意味します。 これにより、コードの後半で名前空間を拡張する必要がある場合に、名前空間に柔軟性を追加できます。
前の例を使用すると、これは、DatabaseEntity
名前空間を再度宣言した場合、より多くのプロパティで名前空間を拡張できることを意味します。 別の名前空間宣言を使用して、新しいクラスUserRole
をDatabaseEntity
名前空間に追加します。
namespace DatabaseEntity {
export class User {
constructor(public name: string) {}
}
export const newUser = new User("Jon");
}
namespace DatabaseEntity {
export class UserRole {
constructor(public user: User, public role: string) {}
}
export const newUserRole = new UserRole(newUser, "admin");
}
新しいDatabaseEntity
名前空間宣言内では、完全修飾名を使用しなくても、以前の宣言を含め、DatabaseEntity
名前空間で以前にエクスポートされた任意のメンバーを使用できます。 UserRole
コンストラクターのuser
パラメーターの型をUser
型に設定するために、最初の名前空間で宣言された名前を使用しています。 newUser
値を使用した新しいUserRole
インスタンス。 これが可能なのは、前の名前空間宣言でそれらをエクスポートしたためです。
名前空間の基本的な構文を確認したので、TypeScriptコンパイラによって名前空間がJavaScriptに変換される方法の調査に進むことができます。
名前空間を使用するときに生成されるJavaScriptコードの調査
TypeScriptの名前空間は、単なるコンパイル時の機能ではありません。 また、結果のJavaScriptコードも変更します。 名前空間がどのように機能するかについて詳しく知るには、このTypeScript機能を強化するJavaScriptを分析できます。 このステップでは、前のセクションのコードスニペットを取得し、その基礎となるJavaScript実装を調べます。
最初の例で使用したコードを見てください。
namespace DatabaseEntity {
export class User {
constructor(public name: string) {}
}
export const newUser = new User("Jon");
}
console.log(DatabaseEntity.newUser.name);
TypeScriptコンパイラは、このTypeScriptスニペットに対して次のJavaScriptコードを生成します。
"use strict";
var DatabaseEntity;
(function (DatabaseEntity) {
class User {
constructor(name) {
this.name = name;
}
}
DatabaseEntity.User = User;
DatabaseEntity.newUser = new User("Jon");
})(DatabaseEntity || (DatabaseEntity = {}));
console.log(DatabaseEntity.newUser.name);
DatabaseEntity
名前空間を宣言するために、TypeScriptコンパイラはDatabaseEntity
という初期化されていない変数を作成し、次に即時呼び出し関数式(IIFE)を作成します。 このIIFEは、DatabaseEntity
変数の現在の値である単一のパラメーターDatabaseEntity || (DatabaseEntity = {})
を受け取ります。 真の値に設定されていない場合は、変数の値を空のオブジェクトに設定します。
DatabaseEntity
の値をIIFEに渡すときに空の値に設定すると、代入操作の戻り値が代入される値になるため、機能します。 この場合、これは空のオブジェクトです。
IIFE内で、User
クラスが作成され、DatabaseEntity
オブジェクトのUser
プロパティに割り当てられます。 newUser
プロパティについても同じことが起こり、プロパティを新しいUser
インスタンスの値に割り当てます。
次に、複数の名前空間宣言があった2番目のコード例を見てください。
namespace DatabaseEntity {
export class User {
constructor(public name: string) {}
}
export const newUser = new User("Jon");
}
namespace DatabaseEntity {
export class UserRole {
constructor(public user: User, public role: string) {}
}
export const newUserRole = new UserRole(newUser, "admin");
}
生成されたJavaScriptコードは次のようになります。
"use strict";
var DatabaseEntity;
(function (DatabaseEntity) {
class User {
constructor(name) {
this.name = name;
}
}
DatabaseEntity.User = User;
DatabaseEntity.newUser = new User("Jon");
})(DatabaseEntity || (DatabaseEntity = {}));
(function (DatabaseEntity) {
class UserRole {
constructor(user, role) {
this.user = user;
this.role = role;
}
}
DatabaseEntity.UserRole = UserRole;
DatabaseEntity.newUserRole = new UserRole(DatabaseEntity.newUser, "admin");
})(DatabaseEntity || (DatabaseEntity = {}));
コードの最初は以前と同じように見えますが、初期化されていない変数DatabaseEntity
があり、次にDatabaseEntity
オブジェクトのプロパティを設定する実際のコードを持つIIFEがあります。 今回は別のIIFEもありますが。 この新しいIIFEは、DatabaseEntity
名前空間の2番目の宣言と一致します。
これで、2番目のIIFEが実行されると、DatabaseEntity
はすでにオブジェクトにバインドされているため、プロパティを追加することで、すでに使用可能なオブジェクトを拡張するだけです。
これで、TypeScript名前空間の構文と、それらが基盤となるJavaScriptでどのように機能するかを確認しました。 このコンテキストで、名前空間の一般的なユースケースを実行できるようになりました。入力せずに外部ライブラリの型を定義します。
名前空間を使用して外部ライブラリに入力を提供する
このセクションでは、名前空間が役立つシナリオの1つである外部ライブラリのモジュール宣言の作成について説明します。 これを行うには、TypeScriptプロジェクトに新しいファイルを書き込んで入力を宣言してから、tsconfig.json
ファイルを変更してTypeScriptコンパイラに型を認識させます。
注:次の手順を実行するには、ファイルシステムにアクセスできるTypeScript環境が必要です。 TypeScript Playgroundを使用している場合は、トップメニューのエクスポートをクリックし、 CodeSandboxで開くをクリックして、既存のコードをCodeSandboxプロジェクトにエクスポートできます。 これにより、新しいファイルを作成し、tsconfig.json
ファイルを編集できます。
npmレジストリで利用可能なすべてのパッケージに独自のTypeScriptモジュール宣言がバンドルされているわけではありません。 つまり、プロジェクトにパッケージをインストールするときに、パッケージの型の宣言がないことに関連するコンパイルエラーが発生するか、すべての型がany
に設定されているライブラリを操作する必要があります。 TypeScriptをどれだけ厳密に使用しているかによっては、これは望ましくない結果になる可能性があります。
うまくいけば、このパッケージには DefinetelyTypedコミュニティによって作成された@types
パッケージが含まれ、パッケージをインストールしてそのライブラリの作業タイプを取得できるようになります。 ただし、これが常に当てはまるわけではなく、独自の型モジュール宣言をバンドルしていないライブラリを処理する必要がある場合もあります。 この場合、コードを完全にタイプセーフに保ちたい場合は、モジュール宣言を自分で作成する必要があります。
例として、単一のメソッドadd
を使用して、単一のクラスVector3
をエクスポートするexample-vector3
というベクトルライブラリを使用しているとします。 このメソッドは、2つのVector3
ベクトルを一緒に追加するために使用されます。
ライブラリ内のコードは次のようになります。
export class Vector3 {
super(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
add(vec) {
let x = this.x + vector.x;
let y = this.y + vector.y;
let z = this.z + vector.z;
let newVector = new Vector3(x, y, z);
return newVector
}
}
これにより、プロパティx
、y
、およびz
を持つベクトルを作成するクラスがエクスポートされます。これは、ベクトルの座標コンポーネントを表すことを目的としています。
次に、架空のライブラリを使用するコードの例を見てみましょう。
import { Vector3 } from "example-vector3";
const v1 = new Vector3(1, 2, 3);
const v2 = new Vector3(1, 2, 3);
const v3 = v1.add(v2);
example-vector3
ライブラリは独自の型宣言にバンドルされていないため、TypeScriptコンパイラでエラー2307
が発生します。
OutputCannot find module 'example-vector3' or its corresponding type declarations. ts(2307)
この問題を修正するために、このパッケージの型宣言ファイルを作成します。 まず、types/example-vector3/index.d.ts
という名前の新しいファイルを作成し、お気に入りのエディターで開きます。 このファイル内に次のコードを記述します。
declare module "example-vector3" {
export = vector3;
namespace vector3 {
}
}
このコードでは、example-vector3
モジュールの型宣言を作成しています。 コードの最初の部分は、declare module
ブロック自体です。 TypeScriptコンパイラは、このブロックを解析し、モジュール自体の型表現であるかのように内部のすべてを解釈します。 これは、ここで宣言するものはすべて、TypeScriptがモジュールのタイプを推測するために使用することを意味します。 今、あなたはこのモジュールがvector3
と呼ばれる単一の名前空間をエクスポートすると言っています。これは現在空です。
このファイルを保存して終了します。
TypeScriptコンパイラは現在、宣言ファイルを認識していないため、tsconfig.json
に含める必要があります。 これを行うには、types
プロパティをcompilerOptions
オプションに追加して、プロジェクトtsconfig.json
を編集します。
{
"compilerOptions": {
...
"types": ["./types/example-vector3/index.d.ts"]
}
}
ここで、元のコードに戻ると、エラーが変更されていることがわかります。 TypeScriptコンパイラでエラー2305
が発生するようになりました。
OutputModule '"example-vector3"' has no exported member 'Vector3'. ts(2305)
example-vector3
のモジュール宣言を作成しましたが、現在、エクスポートは空の名前空間に設定されています。 その名前空間内からエクスポートされているVector3
クラスはありません。
types/example-vector3/index.d.ts
を再度開き、次のコードを記述します。
declare module "example-vector3" {
export = vector3;
namespace vector3 {
export class Vector3 {
constructor(x: number, y: number, z: number);
add(vec: Vector3): Vector3;
}
}
}
このコードでは、vector3
名前空間内でクラスをエクスポートしていることに注目してください。 モジュール宣言の主な目的は、ライブラリによって公開される値の型情報を提供することです。 このようにして、タイプセーフな方法で使用できます。
この場合、example-vector3
ライブラリは、コンストラクターで3つの数値を受け入れ、2つのVector3
インスタンスをまとめて、結果として新しいインスタンスを返します。 ここで実装を提供する必要はなく、タイプ情報自体を提供するだけです。 実装を提供しない宣言は、TypeScriptでは ambient 宣言と呼ばれ、.d.ts
ファイル内に作成するのが一般的です。
このコードは正しくコンパイルされ、Vector3
クラスのタイプが正しくなります。
名前空間を使用すると、ライブラリによってエクスポートされるものを単一のタイプユニット(この場合はvector3
名前空間)に分離できます。 これにより、モジュール宣言を将来的にカスタマイズしたり、DefinetelyTypedリポジトリに送信することですべての開発者が型宣言を利用できるようにしたりすることが容易になります。
結論
このチュートリアルでは、TypeScriptの名前空間の基本的な構文を実行し、TypeScriptコンパイラが変更するJavaScriptを調べました。 また、名前空間の一般的な使用例を試しました。まだ入力されていない外部ライブラリにアンビエント型を提供するためです。
名前空間は非推奨ではありませんが、コードベースのコード編成メカニズムとして名前空間を使用することが常に推奨されるわけではありません。 最新のコードでは、 ESモジュール構文を使用する必要があります。これは、名前空間によって提供されるすべての機能を備えており、ECMAScript2015以降は仕様の一部になりました。 ただし、モジュール宣言を作成する場合は、より簡潔な型宣言が可能になるため、名前空間の使用をお勧めします。
TypeScriptのその他のチュートリアルについては、TypeScriptシリーズのコーディング方法ページをご覧ください。