TypeScript 速習チュートリアル

TypeScript JSDoc

TypeScript を JSDoc と組み合わせて使用することで、JavaScript ファイルを .ts に変換することなく型チェックを追加できます。
これは、コードベースを段階的に移行する場合や、既存の JavaScript プロジェクトに型安全性(Type Safety)を導入したい場合に最適なアプローチです。

1. はじめに

JavaScript ファイルで TypeScript によるチェックを有効にするには、以下の手順が必要です。

  • tsconfig.json ファイルを作成する(未作成の場合)
  • checkJs を有効にするか、個別のファイルの先頭で // @ts-check を使用する

1.1 型安全性のための JSDoc 例

コード例

// @ts-check

/**
* 2つの数値を加算します。
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
  return a + b;
}

2. オブジェクトとインターフェース

2.1 インラインでのオブジェクト型定義

コード例

// @ts-check

/**
* @param {{ firstName: string, lastName: string, age?: number }} person
*/
function greet(person) {
  return `こんにちは、${person.firstName} ${person.lastName}`;
}

greet({ firstName: 'John', lastName: 'Doe' }); // OK
greet({ firstName: 'Jane' }); // エラー: プロパティ 'lastName' が不足しています

2.2 複雑な型に対する @typedef の使用

コード例

// @ts-check

/**
* @typedef {Object} User
* @property {number} id - ユーザーID
* @property {string} username - ユーザー名
* @property {string} [email] - オプショナルなメールアドレス
* @property {('admin'|'user'|'guest')} role - ユーザーロール
* @property {() => string} getFullName - フルネームを返すメソッド
*/

/** @type {User} */
const currentUser = {
  id: 1,
  username: 'johndoe',
  role: 'admin',
  getFullName() {
    return 'John Doe';
  }
};

// TypeScript が User プロパティの補完(Autocomplete)を提供します
console.log(currentUser.role);

2.3 型の拡張

コード例

// @ts-check

/** @typedef {{ x: number, y: number }} Point */

/**
* @typedef {Point & { z: number }} Point3D
*/

/** @type {Point3D} */
const point3d = { x: 1, y: 2, z: 3 };

// @ts-expect-error - z プロパティが不足しているためエラーを期待
const point2d = { x: 1, y: 2 };

3. 関数型

3.1 関数宣言

コード例

// @ts-check

/**
* 長方形の面積を計算します
* @param {number} width - 長方形の幅
* @param {number} height - 長方形の高さ
* @returns {number} 計算された面積
*/
function calculateArea(width, height) {
  return width * height;
}

// TypeScript は引数と戻り値の型を認識します
const area = calculateArea(10, 20);

3.2 関数式とコールバック

コード例

// @ts-check

/**
* @callback StringProcessor
* @param {string} input
* @returns {string}
*/

/**
* @type {StringProcessor}
*/
const toUpperCase = (str) => str.toUpperCase();

/**
* @param {string[]} strings
* @param {StringProcessor} processor
* @returns {string[]}
*/
function processStrings(strings, processor) {
  return strings.map(processor);
}

const result = processStrings(['hello', 'world'], toUpperCase);
// 結果は ['HELLO', 'WORLD'] となります

3.3 関数のオーバーロード

コード例

// @ts-check

/**
* @overload
* @param {string} a
* @param {string} b
* @returns {string}
*/
/**
* @overload
* @param {number} a
* @param {number} b
* @returns {number}
*/
/**
* @param {string | number} a
* @param {string | number} b
* @returns {string | number}
*/
function add(a, b) {
  if (typeof a === 'string' || typeof b === 'string') {
    return String(a) + String(b);
  }
  return a + b;
}

const strResult = add('Hello, ', 'World!'); // string 型
const numResult = add(10, 20); // number 型

4. 高度な型

4.1 ユニオン型とインターセクション型

コード例

// @ts-check

/** @typedef {{ name: string, age: number }} Person */
/** @typedef {Person & { employeeId: string }} Employee */
/** @typedef {Person | { guestId: string, visitDate: Date }} Visitor */

/** @type {Employee} */
const employee = {
  name: 'Alice',
  age: 30,
  employeeId: 'E123'
};

/** @type {Visitor} */
const guest = {
  guestId: 'G456',
  visitDate: new Date()
};

/**
* @param {Visitor} visitor
* @returns {string}
*/
function getVisitorId(visitor) {
  if ('guestId' in visitor) {
    return visitor.guestId; // TypeScript はこれが Visitor (guestId持ち) であることを認識
  }
  return visitor.name; // TypeScript はこれが Person であることを認識
}

4.2 マップ型(Mapped Types)と条件付き型(Conditional Types)

コード例

// @ts-check

/** * @template T * @typedef {[K in keyof T]: T[K] extends Function ? K : never}[keyof T] MethodNames */

/** * @template T * @typedef {{ * [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] * }} Getters */

/** @type {Getters<{ name: string, age: number }> } */
const userGetters = {
  getName: () => 'John',
  getAge: () => 30
};

// TypeScript は戻り値の型を強制します
const name = userGetters.getName(); // string 型
const age = userGetters.getAge(); // number 型

5. 型のインポート

5.1 他のファイルからの型インポート

コード例

// @ts-check

// TypeScript ファイルから型をインポート
/** @typedef {import('./types').User} User */

// node_modules から型をインポート
/** @typedef {import('express').Request} ExpressRequest */

// リネームしてインポート
/** @typedef {import('./api').default as ApiClient} ApiClient */

5.2 宣言ファイル(.d.ts)の作成

プロジェクトに types.d.ts ファイルを作成します。

types.d.ts

declare module 'my-module' {
  export interface Config {
    apiKey: string;
    timeout?: number;
    retries?: number;
  }
  
  export function initialize(config: Config): void;
  export function fetchData<T = any>(url: string): Promise<T>;
}

これを JavaScript ファイルで使用します。

コード例

// @ts-check

/** @type {import('my-module').Config} */
const config = {
  apiKey: '12345',
  timeout: 5000
};

// TypeScript は補完と型チェックを提供します
import { initialize } from 'my-module';
initialize(config);

6. ベストプラクティス

TypeScript と JSDoc を併用する際は、以下のベストプラクティスに従ってください。

  • 型チェックを行いたいファイルの先頭で必ず // @ts-check を有効にする
  • 複数の場所で使用される複雑な型には @typedef を使用する
  • すべての関数の引数と戻り値の型をドキュメント化する
  • ジェネリックな関数や型には @template を使用する
  • 型定義のないサードパーティライブラリには宣言ファイル(.d.ts)を作成する
  • エラーが予想される箇所では @ts-ignore ではなく @ts-expect-error を使用する

7. よくある落とし穴

以下の一般的な問題に注意してください。

  • // @ts-check の欠落: これがないと型チェックは動作しません。
  • 不正確な JSDoc 構文: わずかなタイポ(打ち間違い)で型チェックが無効になることがあります。
  • 型の競合: 異なるソースからの型が一致しない場合に発生します。
  • 推論の問題: TypeScript が型を正しく推論できない場合があります。
  • パフォーマンス: 複雑な型を持つ巨大な JavaScript ファイルは、チェックに時間がかかることがあります。

8. まとめ

TypeScript と JSDoc を組み合わせることで、ファイルを TypeScript に変換することなく、JavaScript プロジェクトに強力な型安全性を追加できます。

このアプローチは、特に以下のような場合に有用です。

  • JavaScript コードベースから TypeScript への段階的な移行
  • 既存の JavaScript プロジェクトへの型チェックの導入
  • .ts ファイルがサポートされていない環境での作業
  • 型情報による JavaScript コードのドキュメント化

本チュートリアルで紹介したパターンとベストプラクティスに従うことで、JavaScript を使い続けながら TypeScript の多くのメリットを享受できます。

リマインダー: JSDoc は優れた型チェックを提供しますが、新規プロジェクトや完全な移行を検討している場合は、最高の開発体験を得るために .ts ファイルの使用を推奨します。