型定義ファイル (.d.ts)
自身のプロジェクトでTypeScriptでコーディングする場合は型を宣言することにより、IDEやエディターの補完機能やコードチェックを行えます。しかし外部のパッケージ(npm)を利用する場合は型定義ファイルが含まれているとは限りません。
型定義ファイルとは
型定義ファイルとはアクセス可能な宣言を記述したファイルです。拡張子は.d.ts
です。
型定義ファイルは主にパッケージを配布するために作成されます。TypeScriptはJavaScriptにコンパイルされるときに型情報は無くなってしまいます。そのままJavaScriptパッケージを利用すると型定義の恩恵を得ることができません。しかし型定義ファイルを同梱することにより補完やコードチェックとして利用することができます。
残念なことにnpmに公開されているすべてのパッケージに必ずしも定義ファイルが存在するとは限りません。こちらに関しては型定義ファイルの有無にて説明します。
型定義ファイル出力例
tscコマンドに-d
オプションをつけてコンパイルを行うとJavaScriptと型定義ファイルを出力することができます。
TypeScriptファイル
次のTypeScriptファイル(sample.ts)を-d
オプションを付けてコンパイルしてみます。
sample.tsts
interfacePerson {firstName : string;lastName : string;}functiongreeter (person :Person ): string {return "Hello, " +person .firstName + " " +person .lastName ;}
sample.tsts
interfacePerson {firstName : string;lastName : string;}functiongreeter (person :Person ): string {return "Hello, " +person .firstName + " " +person .lastName ;}
tscコマンドに-d
オプションを付けコンパイルを実行する。
bash
tsc -d
bash
tsc -d
JavaScriptファイル
sample.tsではInterfaceを使っていますが、JavaScriptにはInterfaceの概念がないため関数のみになりました。また引数の型情報もなくなります。
sample.jsjs
functiongreeter (person ) {return "Hello, " +person .firstName + " " +person .lastName ;}//# sourceMappingURL=sample.js.map
sample.jsjs
functiongreeter (person ) {return "Hello, " +person .firstName + " " +person .lastName ;}//# sourceMappingURL=sample.js.map
d.ts
ファイル
定義情報のみ記載されたファイルが出力されます。
sample.d.tsts
interfacePerson {firstName : string;lastName : string;}declare functiongreeter (person :Person ): string;
sample.d.tsts
interfacePerson {firstName : string;lastName : string;}declare functiongreeter (person :Person ): string;
型定義ファイルの有無
型定義ファイルはパッケージ開発者またはボランティアにより作成されています。
- 型定義ファイル有り
- TypeScriptで書かれたパッケージ
- JavaScriptで書かれたパッケージだが
.d.ts
ファイルを同梱している
- 型定義ファイル有りだが別途インストールが必要
- JavaScriptで書かれたパッケージだが、 DefinitelyTypedに登録されている
- 型定義ファイル無し
- JavaScriptで書かれたパッケージで型定義ファイルが存在しない
型定義ファイル有り
型定義ファイルが含まれているパッケージの場合は特別な作業は必要ありません。
例としてdate libraryのmomentはJavaScriptで構築されていますが、moment.d.ts
を同封しています。そのままinstallを行うだけで定義ファイルの恩恵を受けられます。
bash
npm install moment
bash
npm install moment
型定義ファイル有りの場合は、設定なく型情報を参照することができます。
型定義ファイル有りだが別途インストールが必要
もし、パッケージに型定義ファイルが同梱されていない場合は別途インストールする必要があります。
TypeSearchからパッケージ名を検索しインストールを行います。TypeSearchのリポジトリはDefinitelyTypedであり、ここに多くのライブラリの定義ファイルが一元管理されています。定義ファイルのインストールもnpm
コマンドを利用します。
Express本体と定義ファイルのインストール例は次のようになります。
bash
npm install express --save # express本体のインストールnpm install @types/express --save-dev # 型定義ファイルのインストール
bash
npm install express --save # express本体のインストールnpm install @types/express --save-dev # 型定義ファイルのインストール
型定義ファイル無し
型定義ファイルがないライブラリも存在します。その場合は
any
で妥協する- 型定義ファイルを作る
型定義ファイルの存在しないライブラリも利用することが可能ですが暗黙的にany
型になります。また自身で作成しDefinitelyTypedに公開することもできます。
コントリビュート(貢献)する方法 | Definitely Typed
型定義ファイルで登場するキーワード
ここでは型定義ファイルを読めるようになるために、型定義ファイルでよく利用されるキーワードを紹介します。
declare
declare
キーワードを使うことでTypeScriptに変数、関数、クラスなどがJavaScript内に「存在する」ことを伝えることができます。これを「アンビエント宣言」と呼びます。
次のファイルがJavaScriptライブラリとして読み込まれており、グローバル関数としてhello
が使える状態だとします。
js
functionhello (name ) {return "Hello, " +name ;}
js
functionhello (name ) {return "Hello, " +name ;}
この状態でTypeScriptでhello
関数を呼び出すと型エラーが発生します。これは、TypeScriptがhello
関数が存在することを知らないためです。
ts
Cannot find name 'hello'.2304Cannot find name 'hello'.hello ("taro");
ts
Cannot find name 'hello'.2304Cannot find name 'hello'.hello ("taro");
declare
を利用してアンビエント宣言をすることで、TypeScriptにJavaScript内のどこかにhello
関数が「存在する」ことを宣言することができます。これによりTypeScriptがhello
関数を認識できるようになります。
ts
declare functionhello (name : string): string;hello ("taro");
ts
declare functionhello (name : string): string;hello ("taro");
実際のモジュールの型定義ファイルの例としてjest
の型定義ファイルを見てみましょう。beforeAll
などの関数が型定義ファイル内でアンビエント宣言されているのが確認できます。これによりモジュールの読み込みをしなくても、TypeScriptがbeforeAll
を関数として認識することができます。
node_modules/@types/jest/index.d.tsts
declare varbeforeAll :jest .Lifecycle ;declare namespacejest {typeLifecycle = (fn :ProvidesHookCallback ,timeout ?: number) => any;}
node_modules/@types/jest/index.d.tsts
declare varbeforeAll :jest .Lifecycle ;declare namespacejest {typeLifecycle = (fn :ProvidesHookCallback ,timeout ?: number) => any;}
namespace
namespace
キーワードを使うことで名前空間を定義することができます。
名前空間を定義することで、型名の衝突を避けることができます。
Element
という型をライブラリの型として定義してライブラリ利用者が参照できるようにしたいと考えみます。この型はTypeScriptのlib.dom.d.ts
にすでに定義されているため、そのまま同じグローバルな空間に定義をすると名前が衝突してしまいます。
node_modules/typescript/lib/lib.dom.d.tsts
interfaceElement extendsNode ,ARIAMixin ,Animatable ,ChildNode ,InnerHTML ,NonDocumentTypeChildNode ,ParentNode ,Slottable {readonlyattributes :NamedNodeMap ;/** Allows for manipulation of element's class content attribute as a set of whitespace-separated tokens through a DOMTokenList object. */readonlyclassList :DOMTokenList ;// 省略}
node_modules/typescript/lib/lib.dom.d.tsts
interfaceElement extendsNode ,ARIAMixin ,Animatable ,ChildNode ,InnerHTML ,NonDocumentTypeChildNode ,ParentNode ,Slottable {readonlyattributes :NamedNodeMap ;/** Allows for manipulation of element's class content attribute as a set of whitespace-separated tokens through a DOMTokenList object. */readonlyclassList :DOMTokenList ;// 省略}
次のコードはnamespace
を使わずにライブラリ独自の型としてElement
を定義している例です。TypeScriptでは同じインターフェースが定義された場合は宣言のマージが発生するため、lib.dom.d.ts
で定義されている型とマージされるため、attributes
プロパティなど複数プロパティの指定を求められてしまいます。
ts
// hello.d.tsinterfaceElement {id : string;}// index.tsconstType '{ id: string; }' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 159 more.2740Type '{ id: string; }' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 159 more.: e Element = {id : "1",};
ts
// hello.d.tsinterfaceElement {id : string;}// index.tsconstType '{ id: string; }' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 159 more.2740Type '{ id: string; }' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 159 more.: e Element = {id : "1",};
名前空間を定義することで衝突を避けてライブラリ独自の型を定義をすることができます。
ts
// @filename: hello.d.tsnamespaceHello {interfaceElement {id : number;}}// @filename: index.tsconste :Hello .Element = {id : 1,};
ts
// @filename: hello.d.tsnamespaceHello {interfaceElement {id : number;}}// @filename: index.tsconste :Hello .Element = {id : 1,};
Reactの型定義ファイルでは、次のようにnamespace JSX
で名前空間が定義されてElement
の型が定義がされています。
declare global
と declare namespace
の違いについて
型定義ファイルでは同じ振る舞いをするため違いはない。declare global
と記述をすることで、グローバルスコープに名前空間を定義するということを開発者の意図として明示できる?
ts
// @filename: node_modules/@types/react/index.d.tsdeclareglobal {namespaceJSX {interfaceElement extendsReact .ReactElement <any, any> {}// 省略}}
ts
// @filename: node_modules/@types/react/index.d.tsdeclareglobal {namespaceJSX {interfaceElement extendsReact .ReactElement <any, any> {}// 省略}}
module
TypeScript1.5以前では、module
キーワードが「内部モジュール(名前空間)」を定義するために使用されていました。これは現在のnamespace
の機能と同等です。しかし、この名前がESModuleの「外部モジュール」の定義とキーワード名が重複し、混乱を招いてしまう可能性があったため、TypeScript1.5から「内部モジュール」は「名前空間」と呼ばれるように変更され、namespace
キーワードが新たに導入されました。
現在では、module
キーワードは非推奨となっているため、namespace
キーワードの使用をするようにしてください。
トリプルスラッシュ・ディレクティブ
型定義ファイルの先頭で見かける3つのスラッシュ(///
)ではじめるコメント行をトリプルスラッシュ・ディレクティブと呼びます。これは、TypeScript独自の形式でコンパイラに対して指示を出す機能を持っています。
トリプルスラッシュ・ディレクティブにはいくつかの種類が存在しており、ここでは多くの型定義ファイルで目にする代表的なディレクティブを2つ紹介します。
/// <reference path="..." />
(参照ディレクティブ)
参照ディレクティブはコンパイラに型定義ファイル間の依存関係を宣言でき、path
で指定された型定義ファイルを追加でコンパイル時に読み込むように指示を与えることができます。たとえば、次の例ではindex.d.ts
をコンパイラが読み込む際に追加でglobal.d.ts
を読み込みます。
node_modules/@types/react/index.d.tsts
/// <reference path="global.d.ts" />
node_modules/@types/react/index.d.tsts
/// <reference path="global.d.ts" />
/// <reference types="..." />
(型ディレクティブ)
型ディレクティブはnpmパッケージへの依存関係を宣言できます。宣言されたパッケージの依存を解決する処理はimport文でのパッケージの解決と似た処理のため、型ディレクティブは型のimportのようなものとも考えられます。
次の例はexpressの型定義ファイルの一部です。型ディレクティブでserve-static
パッケージの型定義ファイルに依存していることが示されています。
node_modules/@types/express/index.d.tsts
/// <reference types="express-serve-static-core" />/// <reference types="serve-static" />
node_modules/@types/express/index.d.tsts
/// <reference types="express-serve-static-core" />/// <reference types="serve-static" />