TypeScriptの基礎 - モジュールと型のインポート

提供: MochiuWiki : SUSE, EC, PCB

概要

TypeScriptにおけるモジュールと型のインポートは、型安全なコードを記述する上で重要な仕組みである。

TypeScript 3.8で導入された import type 構文を使用することにより、型情報のみをインポートでき、JavaScriptランタイムには一切影響を与えない。
これにより、バンドルサイズの削減やサイドエフェクトの回避が可能となる。

型定義ファイル (.d.ts) は、型情報を宣言するためのアンビエント宣言専用ファイルであり、ライブラリの型情報を別ファイルとして提供する時に使用される。
declare キーワードと組み合わせることにより、外部JavaScriptライブラリや環境固有のグローバル変数に対して型情報を付与できる。

サードパーティライブラリの型定義は、DefinitelyTypedが管理する @types パッケージとして提供されており、npm経由でインストールできる。
型定義が存在しないライブラリに対しては、declare module によるシム定義や独自の型定義ファイルの作成により対処できる。

TypeScript 5.0では verbatimModuleSyntax オプションが導入され、型インポートの明示が必須となった。
TypeScript 6.0ベータでは strict: truemodule: "esnext" がデフォルトとなり、types: [] がデフォルトに変更されたため、
必要なパッケージの明示的な指定が求められる。

なお、TypeScript 7.0 (Project Corsa) ではGo言語による書き直しにより10倍のコンパイル速度向上が予定されているが、言語仕様への変更はない。

下表に、モジュールと型インポートに関する主要な概念を示す。

モジュールと型インポートの主要概念
概念 概要
import type 型情報のみをインポートするTypeScript 3.8以降の構文
JavaScriptランタイムには影響を与えない。
インライン型インポート import { type Foo, bar } のように型と値を1つのimport文に混在させる構文
TypeScript 4.5以降で使用可能
.d.tsファイル 型宣言専用ファイル
JavaScriptコードは含まず、型情報のみを記述する。
declare キーワード アンビエント宣言を行うキーワード
JSコードを生成せず、型情報としてのみ機能する。
declare module 既存モジュールの型定義を宣言または拡張するための構文
declare global グローバル名前空間に型を追加するための構文
@typesパッケージ DefinitelyTypedが管理するコミュニティ製型定義パッケージ
npmで提供される。
verbatimModuleSyntax TypeScript 5.0で導入
import / export文をそのまま出力し、型インポートの明示を強制するオプション



import typeによる型のインポート

import type 構文は、型情報のみをインポートするための専用構文である。
通常の import と異なり、コンパイル後のJavaScriptには何も出力されないため、ランタイムへの影響がない。

基本的な構文

import type の基本構文を以下に示す。

 // 名前付き型のインポート
 import type { Foo, Bar } from "./module";
 
 // デフォルト型のインポート
 import type DefaultType from "./module";
 
 // 名前空間型のインポート
 import type * as Types from "./module";


型と値を同じモジュールからインポートする場合は、import type と通常の import を別々に記述する。

 import type { User, Role } from "./types";
 import { fetchUser, createUser } from "./api";
 
 function getUser(id: string): Promise<User> {
    return fetchUser(id);
 }


export type を使用して、型のみをエクスポートすることもできる。

 // 型のみのエクスポート
 export type { MyType, AnotherType };
 
 // 型エイリアスの定義とエクスポート
 export type Point = { x: number; y: number };


import typeの利点

import type を使用することで得られるメリットを以下に示す。

import typeを使用するメリット
メリット 説明
Tree-shakingの効率化 バンドラーが不要な型を除去しやすくなり、最終的なバンドルサイズが削減される。
型情報はランタイムには存在しないため、バンドル対象から確実に除外される。
サイドエフェクトの防止 通常の import はモジュールの初期化コードを実行する場合があるが、
import type はモジュールの実行を伴わない。
意図しないモジュール初期化を防止できる。
循環依存の解決 型情報のみのインポートはランタイムには関係しないため、
循環参照が発生してもJavaScriptの実行に影響を与えない。
コード意図の明確化 import type と記述することで、
そのインポートが型情報のみであることが一目で分かる。


下表に、importimport type の違いを示す。

import と import typeの違い
項目 import import type
JavaScriptへの出力 コンパイル後も残る コンパイル後に完全削除される。
モジュールの実行 モジュールの初期化コードが実行される モジュールの実行を伴わない。
使用可能な対象 型・値・クラス・関数など全て 型とインターフェースのみ
Tree-shaking バンドラーの判断に依存する 確実に除外される。
循環依存への影響 ランタイムに影響を与える可能性がある ランタイムに影響を与えない。
verbatimModuleSyntax有効時 値のインポートに使用する 型のインポートに必須


インライン型インポート

TypeScript 4.5以降では、単一の import 文の中で型と値を混在させるインライン型インポートが利用できる。
type キーワードを各識別子の前に付けることで、その識別子が型としてのみ使用されることを示す。

 // TypeScript 4.5以降 : インライン型インポート
 import { type Foo, bar, type Baz } from "./module";
 
 // 実際の使用例
 import { type User, type Role, fetchUser, createRole } from "./api";
 
 function assignRole(user: User, role: Role): void {
    // fetchUser と createRole は値として使用可能
    // User と Role は型としてのみ使用可能
 }


verbatimModuleSyntax オプションが有効な場合、型としてのみ使用されるインポートには type キーワードの付与が必須となる。

 // tsconfig.json で verbatimModuleSyntax: true の場合
 // 型インポートには必ずtypeキーワードを付与する
 import { type ApiResponse, fetchData } from "./api";



型定義ファイル (.d.ts)

型定義ファイル (.d.ts) は、型情報のみを記述するアンビエント宣言専用のファイルである。
JavaScriptコードは含まず、TypeScriptコンパイラが型チェックに使用するための型情報を提供する。

.d.tsファイルとは

.d.tsファイルは、以下の目的で使用される。

  • JavaScriptライブラリに型情報を付与する。
    JavaScriptで記述されたライブラリに対して、TypeScriptが型チェックを行えるよう型情報を別ファイルとして提供する。
  • コンパイル済みコードに型情報を付与する。
    TypeScriptのソースコードをコンパイルした時に自動生成され、ライブラリ利用者がIDEの補完機能を活用できるようにする。
  • アンビエント宣言の定義
    グローバル変数や環境固有のAPIに対して型情報を宣言する。


.d.tsファイルには実装コードを含めることができないため、関数の本体やクラスの実装を記述することはできない。
全ての宣言は declare キーワードを使用したアンビエント宣言として記述する。

.d.tsファイルの基本構文

.d.tsファイルに記述できる代表的な宣言の例を以下に示す。

 // 変数の宣言
 declare const VERSION: string;
 declare let currentUser: string | null;
 
 // 関数の宣言
 declare function greet(name: string): string;
 declare function fetchData(url: string): Promise<unknown>;
 
 // インターフェースの宣言
 declare interface Config {
    host: string;
    port: number;
    debug?: boolean;
 }
 
 // 型エイリアスの宣言
 declare type ID = string | number;
 
 // クラスの宣言
 declare class EventEmitter {
    on(event: string, listener: (...args: any[]) => void): this;
    emit(event: string, ...args: any[]): boolean;
 }
 
 // 名前空間の宣言
 declare namespace MyLib {
    function create(options: Config): EventEmitter;
    const version: string;
 }


自動生成と手動作成

.d.tsファイルはTypeScriptコンパイラによる自動生成 または 手動による作成のいずれかで用意する。

自動生成を行う場合は、tsconfig.jsonファイル に以下の設定を追加する。

 {
    "compilerOptions": {
       "declaration": true,
       "outDir": "./dist",
       "declarationDir": "./dist/types",
       "declarationMap": true,
       "emitDeclarationOnly": false
    }
 }


各オプションの説明を以下に示す。

型宣言ファイル生成オプションの一覧
オプション 説明
declaration: true .d.tsファイルを自動生成する。
declarationDir 生成した.d.tsファイルの出力先ディレクトリを指定する。
declarationMap: true .d.tsファイルとソースファイルを対応付けるソースマップを生成する。
IDEでの定義ジャンプに使用される。
emitDeclarationOnly: true .d.tsファイルのみを出力し、JavaScriptファイルは出力しない。


手動で.d.tsファイルを作成する場合は、ファイル名の末尾を .d.ts とし、declare キーワードを使用したアンビエント宣言のみを記述する。


declareキーワード

declare キーワードは、TypeScriptにおけるアンビエント宣言を記述するためのキーワードである。
アンビエント宣言は型情報としてのみ機能し、JavaScriptコードを生成しない。

アンビエント宣言の基本

declare キーワードを使用することにより、外部で定義された変数・関数・クラスの型情報を宣言できる。

 // 外部スクリプトで定義されたグローバル変数
 declare const API_KEY: string;
 declare let globalState: { [key: string]: any };
 
 // 外部ライブラリの関数
 declare function setTimeout(callback: () => void, ms?: number): number;
 declare function clearTimeout(id: number): void;
 
 // 外部ライブラリのクラス
 declare class Date {
    constructor();
    constructor(value: string | number);
    getTime(): number;
    toISOString(): string;
    static now(): number;
 }


declare で宣言された変数や関数は、実際の実装がJavaScript側に存在することを前提としている。
宣言のみを記述し、値の割り当てや実装は含めない。

declare module

declare module は、モジュールの型定義を宣言または既存モジュールの型定義を拡張するための構文である。

モジュールの型定義を新規に宣言する例を以下に示す。

 // 既存のJavaScriptライブラリに型を付与する
 declare module "legacy-library" {
    export interface Config {
       debug: boolean;
       timeout: number;
    }
    export function initialize(config: Config): void;
    export function destroy(): void;
 }


既存モジュールの型定義を拡張する例を以下に示す。
この手法をモジュール拡張 (Module Augmentation) と呼ぶ。

 // expressモジュールのRequestインターフェースを拡張する
 declare module "express" {
    interface Request {
       userId?: string;
       user?: { id: string; email: string };
    }
 }


ワイルドカードを使用して、特定の拡張子を持つファイルをモジュールとして扱う型定義も記述できる。

 // JSONファイルをモジュールとしてインポート可能にする
 declare module "*.json" {
    const value: any;
    export default value;
 }
 
 // SVGファイルをReactコンポーネントとしてインポート可能にする
 declare module "*.svg" {
    const content: React.FC<React.SVGProps<SVGSVGElement>>;
    export default content;
 }
 
 // CSSファイルをモジュールとしてインポート可能にする
 declare module "*.css" {
    const styles: { [className: string]: string };
    export default styles;
 }


declare global

declare global は、グローバル名前空間に型を追加するための構文である。
モジュールファイル (import / exportを含むファイル) の中でグローバルな型を拡張する場合に使用する。

 declare global {
    // WebブラウザのWindowオブジェクトを拡張する
    interface Window {
       dataLayer: any[];
       gtag: (...args: any[]) => void;
    }
 
    // Node.jsの環境変数に型を付与する
    namespace NodeJS {
       interface ProcessEnv {
          NODE_ENV: "development" | "production" | "test";
          DATABASE_URL: string;
          API_BASE_URL: string;
       }
    }
 
    // グローバル変数の宣言 (varのみ有効、let/constはモジュールローカルになる)
    var __DEV__: boolean;
 }
 
 // モジュールとして認識させるためにexportが必要
 export {};


※注意
declare global ブロック内で「グローバル変数」を宣言する場合は、var を使用しなければならない。
letconst はモジュールローカルとなり、グローバルスコープには追加されない。


サードパーティライブラリの型定義 (@types)

TypeScriptでサードパーティのJavaScriptライブラリを使用する時、多くのライブラリは型定義ファイルを提供していない。
このような場合に対応するために、DefinitelyTypedプロジェクトが管理する @types パッケージが利用できる。

DefinitelyTypedと@typesパッケージ

DefinitelyTypedは、JavaScriptライブラリの型定義をコミュニティが管理する大規模リポジトリである。
収録されている型定義は @types/ プレフィックスを付けたnpmパッケージとして公開されており、npm install コマンドでインストールできる。

下表に、@typesパッケージの主な特徴を示す。

@typesパッケージの主な特徴
特徴 説明
コミュニティによる管理 型定義はGitHubのDefinitelyTypedリポジトリで管理され、プルリクエストにより更新される。
開発依存としてのインストール 型情報はコンパイル時にのみ必要なため、--save-dev フラグを付けてインストールする。
バージョン管理 ライブラリのバージョンに対応する型定義パッケージが提供される場合がある。


@typesパッケージのインストールと使用

代表的な@typesパッケージのインストール方法を以下に示す。

# Node.js APIの型定義
npm install --save-dev @types/node

# Reactの型定義
npm install --save-dev @types/react @types/react-dom

# Expressの型定義
npm install --save-dev @types/express

# jQueryの型定義
npm install --save-dev @types/jquery


インストール後は、TypeScriptコンパイラが自動的に node_modules/@typesディレクトリ を参照するため、追加の設定なしに型情報が使用可能となる。

 // @types/nodeをインストール後、Node.js APIに型が付与される
 import * as fs from "fs";
 import * as path from "path";
 
 const filePath: string = path.join(__dirname, "data.json");
 const content: Buffer = fs.readFileSync(filePath);


typeRootsとtypesの設定

tsconfig.jsonファイルの typeRoots および types オプションを使用して、型定義の探索範囲を制御できる。

 {
    "compilerOptions": {
       "typeRoots": [
          "./src/types",
          "./node_modules/@types"
       ],
       "types": ["node", "jest", "react"]
    }
 }


下表に、各オプションの説明を示す。

型定義ファイル探索オプションの一覧
オプション 説明
typeRoots 型定義ファイルを探索するディレクトリのリストを指定する。
指定した場合、node_modules/@types は自動的に探索されなくなるため、
明示的に追加する必要がある。
types 自動的にインクルードする @types パッケージ名のリストを指定する。
TypeScript 6.0以降ではデフォルト値が空配列([])に変更されたため、
使用するパッケージを明示的に指定する必要がある。



型定義が存在しない場合の対処

@types パッケージが存在しないライブラリや型定義が不完全なライブラリを使用する場合、いくつかの対処方法がある。

下表に、型定義が存在しない場合の対処法の比較を示す。

型定義が存在しない場合の対処法の比較
対処法 型安全性 実装コスト 推奨される場面
declare moduleによるシム定義 中程度 低〜中 利用するAPIが限定的な場合
独自の型定義ファイルの作成 高い 高い 長期利用・チーム開発での使用
any型へのフォールバック 低い 最低限 一時的な対処・プロトタイプ開発
skipLibCheck: true 影響なし 設定のみ 型定義ファイルのエラーを無視したい場合


declare moduleによるシム定義

declare module を使用して、型定義が存在しないライブラリの型をシムとして定義できる。
プロジェクト内に型定義ファイルを作成し、必要なAPIの型情報を記述する。

 // src/types/shims.d.tsファイル
 
 declare module "legacy-library" {
    export interface Config {
       debug: boolean;
       timeout: number;
       retryCount?: number;
    }
 
    export function initialize(config: Config): void;
    export function destroy(): void;
    export function getVersion(): string;
 }
 
 declare module "another-untyped-lib" {
    export default function process(input: string): string;
 }


作成した型定義ファイルをTypeScriptが認識するよう、tsconfig.jsonファイルの include または typeRoots に追加する。

独自の型定義ファイルの作成

ライブラリを長期的に使用する場合やチーム開発で型安全性を維持したい場合は、詳細な型定義ファイルを作成することが推奨される。

 // src/@types/my-custom-lib/index.d.tsファイル
 
 export interface InitOptions {
    apiKey: string;
    endpoint: string;
    timeout?: number;
 }
 
 export interface Data {
    id: string;
    payload: Record<string, unknown>;
    timestamp: number;
 }
 
 export interface MyCustomLib {
    initialize(options: InitOptions): void;
    getData(): Promise<Data>;
    postData(data: Partial<Data>): Promise<{ success: boolean }>;
    destroy(): void;
 }
 
 declare const myCustomLib: MyCustomLib;
 export default myCustomLib;


独自の型定義ファイルを src/@types ディレクトリに配置する場合、tsconfig.jsonファイルに以下の設定を追加する。

 {
    "compilerOptions": {
       "typeRoots": [
          "./src/@types",
          "./node_modules/@types"
       ]
    }
 }


any型へのフォールバック

型定義の作成が困難な場合や一時的な対処として、any 型を使用したシム定義を行うことができる。
型安全性は失われるが、コンパイルエラーを解消して開発を継続することができる。

 // src/types/fallback.d.tsファイル
 
 // モジュール全体をany型として宣言する
 declare module "untyped-library" {
    const content: any;
    export = content;
 }
 
 // 特定の関数のみをany型として宣言する
 declare module "partially-typed-lib" {
    export function knownFunction(input: string): string;
    export const unknownFeature: any;
 }


型定義ファイル自体のエラーを無視したい場合は、tsconfig.jsonファイルに skipLibCheck: true を設定する。
しかし、skipLibCheck は型定義ファイルの型チェックを完全にスキップするため、使用は必要最低限にとどめることが推奨される。

 {
    "compilerOptions": {
       "skipLibCheck": true
    }
 }



関連情報