宣言ファイル理論:深掘り
モジュールを構造化して、目的の正確なAPI形状を実現するのは難しい場合があります。たとえば、`new`の有無に関係なく呼び出して異なる型を生成でき、階層構造で公開されるさまざまな名前付き型を持ち、モジュールオブジェクトにもいくつかのプロパティを持つモジュールが必要になる場合があります。
このガイドを読むことで、フレンドリーなAPIサーフェスを公開する複雑な宣言ファイルを作成するためのツールを手に入れることができます。このガイドは、モジュール(またはUMD)ライブラリに焦点を当てています。なぜなら、ここではオプションがより多様だからです。
キーコンセプト
TypeScriptがどのように動作するかについてのいくつかのキーコンセプトを理解することで、あらゆる形状の宣言をどのように作成するかを完全に理解できます。
型
このガイドを読んでいるということは、おそらくTypeScriptでの型が何であるかを大まかに理解しているでしょう。ただし、より明確にするために、型は次のもので導入されます。
- 型エイリアス宣言 (
type sn = number | string;) - インターフェイス宣言 (
interface I { x: number[]; }) - クラス宣言 (
class C { }) - 列挙型宣言 (
enum E { A, B, C }) - 型を参照する
import宣言
これらの宣言形式のそれぞれが、新しい型名を作成します。
値
型と同様に、おそらく値が何であるかをすでに理解しているでしょう。値は、式で参照できるランタイム名です。たとえば、`let x = 5;`は`x`という値を作成します。
繰り返しますが、明確にするために、以下のものが値を作成します。
let、const、およびvar宣言- 値を含む
namespaceまたはmodule宣言 enum宣言class宣言- 値を参照する
import宣言 function宣言
名前空間
型は名前空間内に存在できます。たとえば、let x: A.B.C という宣言がある場合、型 C は A.B 名前空間から来ていると言います。
この区別は微妙で重要です。ここで、A.B は必ずしも型または値である必要はありません。
単純な組み合わせ:1 つの名前、複数の意味
名前 A が与えられた場合、A には最大 3 つの異なる意味が見つかる可能性があります。型、値、または名前空間です。名前がどのように解釈されるかは、使用されるコンテキストによって異なります。たとえば、宣言 let m: A.A = A; では、A は最初に名前空間として、次に型名として、最後に値として使用されます。これらの意味は、最終的にまったく異なる宣言を参照することになる可能性があります。
これは紛らわしく思えるかもしれませんが、過度にオーバーロードしなければ、実際には非常に便利です。この組み合わせ動作のいくつかの便利な側面を見てみましょう。
組み込みの組み合わせ
注意深い読者なら、たとえば、型と値の両方のリストに class が現れていることに気づくでしょう。宣言 class C { } は、クラスのインスタンス形状を参照する型 C と、クラスのコンストラクター関数を参照する値 C の 2 つを作成します。enum 宣言も同様に動作します。
ユーザー定義の組み合わせ
モジュールファイル foo.d.ts を記述したとしましょう
tsexport var SomeVar: { a: SomeType };export interface SomeType {count: number;}
それを消費したとしましょう
tsimport * as foo from "./foo";let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
これで十分機能しますが、SomeType と SomeVar は非常に密接に関連しており、同じ名前にしたいと想像できるかもしれません。組み合わせを使用すると、これらの 2 つの異なるオブジェクト(値と型)を同じ名前 Bar で表示できます
tsexport var Bar: { a: Bar };export interface Bar {count: number;}
これは、消費側のコードで分割代入を行う絶好の機会となります
tsimport { Bar } from "./foo";let x: Bar = Bar.a;console.log(x.count);
ここでも、Bar を型と値の両方として使用しました。Bar 値が Bar 型であると宣言する必要がなかったことに注意してください。それらは独立しています。
高度な組み合わせ
一部の種類の宣言は、複数の宣言にわたって組み合わせることができます。たとえば、class C { } と interface C { } は共存でき、両方とも C 型にプロパティを提供できます。
これは、競合が発生しない限り合法です。一般的な経験則として、値は、namespace として宣言されていない限り、同じ名前の他の値と常に競合し、型は、型エイリアス宣言 (type s = string) で宣言されている場合に競合し、名前空間は決して競合しないということです。
これがどのように使用できるかを見てみましょう。
interface を使用して追加する
別の interface 宣言で interface にメンバーを追加できます
tsinterface Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
これはクラスでも機能します
tsclass Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
インターフェイスを使用して型エイリアス (type s = string;) に追加できないことに注意してください。
namespace を使用して追加する
namespace 宣言を使用して、競合が発生しない範囲で、新しい型、値、および名前空間を追加できます。
たとえば、クラスに静的メンバーを追加できます
tsclass C {}// ... elsewhere ...namespace C {export let x: number;}let y = C.x; // OK
この例では、C の静的側 (コンストラクター関数) に値を追加しました。これは、値を追加し、すべての値のコンテナーが別の値であるためです (型は名前空間に格納され、名前空間は他の名前空間に格納されます)。
名前空間付きの型をクラスに追加することもできます
tsclass C {}// ... elsewhere ...namespace C {export interface D {}}let y: C.D; // OK
この例では、namespace 宣言を記述するまで、名前空間 C は存在しませんでした。名前空間としての C の意味は、クラスによって作成された C の値または型の意味と競合しません。
最後に、namespace 宣言を使用して、さまざまなマージを実行できます。これは特に現実的な例ではありませんが、あらゆる種類の興味深い動作を示しています
tsnamespace X {export interface Y {}export class Z {}}// ... elsewhere ...namespace X {export var Y: number;export namespace Z {export class C {}}}type X = string;
この例では、最初のブロックは次の名前の意味を作成します
- 値
X(namespace宣言に値Zが含まれているため) - 名前空間
X(namespace宣言に型Yが含まれているため) X名前空間内の型YX名前空間内の型Z(クラスのインスタンス形状)X値のプロパティである値Z(クラスのコンストラクター関数)
2 番目のブロックは、次の名前の意味を作成します
X値のプロパティである値Y(型number)- 名前空間
Z X値のプロパティである値ZX.Z名前空間内の型CX.Z値のプロパティである値C- 型
X