TypeScriptの開発において、型の安全性とコードの柔軟性を両立させることは非常に重要なテーマです。

TypeScriptにはJavaScript譲りの「typeof」演算子が存在しますが、型コンテキストにおいて使用される場合の挙動はJavaScriptのそれとは大きく異なります。

値から型を自動的に導出するこの機能は、コードの二重管理を防ぎ、変更に強いプログラムを構築するための強力な武器となります。

本記事では、基本的な使い方から、現場で即戦力となる応用テクニックまで、実例を交えて詳しく解説します。

TypeScriptにおけるtypeof演算子の役割

TypeScriptのtypeof演算子には、主に2つの側面があることを理解する必要があります。

一つはJavaScriptの標準機能として実行時に動作する「値としての評価」であり、もう一つはTypeScript独自の「型としての評価」です。

プログラミングを進める上で、これら2つの使い分けを意識することは混乱を避けるための第一歩となります。

JavaScriptのtypeofとの決定的な違い

JavaScriptにおけるtypeofは、実行時に変数のデータ型を文字列として返す演算子です。

例えば、typeof "hello""string"という文字列を返します。

一方で、TypeScriptの型アノテーションの文脈で使われるtypeofは、変数やプロパティの「型」そのものを抽出する役割を果たします。

この違いを理解するために、以下のコード例を見てみましょう。

TypeScript
// JavaScriptとしての実行時の動作
const message = "Hello World";
const jsType = typeof message; 

// TypeScriptとしての型抽出の動作
type MessageType = typeof message;
実行結果
jsType の値は "string" (文字列)
MessageType の型は string (型)

このように、同じキーワードであっても記述する場所によって得られる結果が全く異なります。

TypeScriptのtypeofを活用することで、定義済みの値に基づいて型を動的に生成できるため、DRY (Don’t Repeat Yourself) 原則を維持しやすくなります。

基本的な使い方:変数の型を抽出する

最も基本的な使い方は、既に定義されている変数からその型を借りてくる手法です。

これにより、同じ構造を持つ変数を複数定義する際に、型を再定義する手間を省くことができます。

プリミティブ値への適用

数値や文字列などのプリミティブ値に対してtypeofを使用すると、そのリテラルの型や基本型を取得できます。

初期値を持つ定数から型を作成する場合に便利です。

TypeScript
const defaultPort = 8080;
// defaultPortから型を抽出
type PortType = typeof defaultPort;

const myPort: PortType = 3000;

この場合、PortTypenumber型として扱われます。

もしconstではなくletで宣言された変数から抽出した場合も、基本的には推論された型が継承されます。

オブジェクトへの適用

オブジェクトに対してtypeofを使用すると、そのオブジェクトの持つすべてのプロパティ構造を型として抽出できます。

これは、APIのレスポンスのサンプルや、複雑な設定オブジェクトの型を定義する際に非常に強力です。

TypeScript
const userConfig = {
    id: 1,
    name: "Taro",
    isAdmin: true,
    tags: ["developer", "typescript"]
};

// オブジェクトの構造をそのまま型にする
type UserConfig = typeof userConfig;

const newUser: UserConfig = {
    id: 2,
    name: "Hanako",
    isAdmin: false,
    tags: ["designer"]
};

手動でinterfaceを定義しなくても、既存の実装から型を生成できるため、実装と型の乖離を防ぐことができます。

特に、外部から取得したJSONデータを初期値として持つオブジェクトがある場合、その構造を即座に型として利用できる点は大きなメリットです。

発展編:as constとの組み合わせ

TypeScriptのtypeofをさらに強力にするのが、as const (constアサーション) との組み合わせです。

通常のオブジェクトにtypeofを使用すると、プロパティはstringnumberといった広い型として推論されます。

しかし、as constを付与することで、各値を読み取り専用のリテラル型として固定することができます。

TypeScript
const theme = {
    primary: "#007bff",
    secondary: "#6c757d",
    spacing: 8
} as const;

// 各プロパティが特定の値に固定された型になる
type Theme = typeof theme;

このTheme型において、primaryプロパティは単なるstringではなく、"#007bff"という特定の値のみを許可する型になります。

デザインシステムやステータス定義など、変更されるべきではない定数群の型を定義する際に最適です。

定石テクニック:keyof typeofの活用

TypeScriptの中級者以上が頻繁に使用するテクニックの一つに、keyof typeofの組み合わせがあります。

これは、「あるオブジェクトのキー名だけをユニオン型として抽出したい」という場合に非常に役立ちます。

辞書型オブジェクトのキーを型にする

例えば、アプリケーション内で使用する言語リソースのキーを厳密に管理したい場合を考えます。

TypeScript
const MESSAGES = {
    SUCCESS: "操作は成功しました",
    ERROR: "エラーが発生しました",
    LOADING: "読み込み中です"
} as const;

// オブジェクトのキーをユニオン型として抽出
type MessageKeys = keyof typeof MESSAGES;

function showNotification(key: MessageKeys) {
    console.log(MESSAGES[key]);
}

// 正常な呼び出し
showNotification("SUCCESS");

// コンパイルエラー:存在しないキー
// showNotification("WARNING");
実行結果
MessageKeys の型は "SUCCESS" | "ERROR" | "LOADING" になります。

このパターンの利点は、MESSAGESオブジェクトに新しい項目を追加するだけで、自動的にMessageKeys型も更新される点にあります。

型定義ファイルを書き直す必要がなく、ソースコードのメンテナンス性が大幅に向上します。

関数におけるtypeofの活用法

typeofは変数やオブジェクトだけでなく、関数に対しても使用可能です。

関数のシグネチャ (引数の型や戻り値の型) を他の場所で再利用したい場合に便利です。

関数の型をコピーする

特定の関数の構造をそのまま型の宣言として扱いたい場合、以下のように記述します。

TypeScript
function add(a: number, b: number): number {
    return a + b;
}

// 関数の型を抽出
type BinaryOperation = typeof add;

const multiply: BinaryOperation = (x, y) => x * y;

これにより、multiply関数がadd関数と同じ引数と戻り値の制約を持つことが保証されます。

ReturnTypeとの併用

TypeScriptの標準ユーティリティ型であるReturnTypetypeofを組み合わせることで、「ある関数が返す値の型」を動的に取得できます。

TypeScript
function createUser() {
    return {
        id: 1,
        profile: {
            name: "Alice",
            age: 25
        }
    };
}

// 関数の戻り値の型を抽出
type UserResponse = ReturnType<typeof createuser="">;

const user: UserResponse = {
    id: 2,
    profile: {
        name: "Bob",
        age: 30
    }
};
</typeof>

大規模なアプリケーションでは、複雑なファクトリ関数やAPI通信関数の戻り値を型として利用する場面が多いため、このテクニックは必須と言えます。

関数の実装を変更した際に、戻り値の型も自動追随するため、型定義の修正漏れによるバグを防ぐことができます。

実践的なユースケース

ここからは、より現場に近いシチュエーションでのtypeofの活用方法を見ていきましょう。

設定ファイルからの型定義

プロジェクト全体の構成を定義した設定ファイルから型を生成する例です。

TypeScript
const APP_CONFIG = {
    apiEndpoint: "https://api.example.com",
    retryLimit: 3,
    features: {
        darkMode: true,
        betaTesting: false
    }
} as const;

export type AppConfig = typeof APP_CONFIG;

function initializeApp(config: AppConfig) {
    // 設定に基づいた初期化処理
}

このように定義しておけば、設定値を変更・追加するだけで、アプリケーション全体の型チェックが最新の状態に保たれます。

外部ライブラリのレスポンス定義

ライブラリが提供する関数の戻り値に型が定義されていない場合や、内部的なオブジェクトを型として流用したい場合にもtypeofが活躍します。

特定の外部SDKの初期化オブジェクトなどの構造を模倣したい場合に非常に有効です。

使用目的構文得られるメリット
値から型を作るtype T = typeof val実装と型定義の同期が容易になる
キーの抽出keyof typeof objオブジェクトのキーを安全に制約できる
戻り値の抽出ReturnType<typeof fn>関数の実行結果に型を付けられる

typeof使用時の注意点と制限事項

非常に便利なtypeofですが、いくつかの注意点があります。

まず、typeof「既に存在している値」に対してのみ使用できるという点です。

型定義が先にある場合には使用できませんので、設計の順序を考慮する必要があります。

また、あまりに多用しすぎると、コードの「真実のソース」がどこにあるのかが分かりにくくなる可能性があります。

基本的には、定数や設定値、あるいはサードパーティ製のライブラリなどで型が公開されていない場合に限定して使用するのが、読みやすいコードを保つコツです。

また、クラスに対してtypeofを使用する場合は注意が必要です。

クラス名そのものは「インスタンスの型」を指しますが、typeof クラス名は「クラスのコンストラクタ関数 (静的部分) の型」を指すことになります。

TypeScript
class Logger {
    static level = "info";
    log(msg: string) { console.log(msg); }
}

// Loggerクラス自体の型 (コンストラクタや静的プロパティを含む)
type LoggerClass = typeof Logger;

// Loggerのインスタンスの型
type LoggerInstance = Logger;

この違いを混同すると、期待通りの型チェックが行われないため、クラスを扱う際はどちらの型を必要としているのかを明確にしましょう。

まとめ

TypeScriptにおけるtypeof演算子は、JavaScriptの機能を拡張し、型システムの中で値を最大限に活用するための強力なブリッジです。

変数やオブジェクトから型を抽出する基本操作から、keyofReturnTypeと組み合わせた応用まで、その用途は多岐にわたります。

これらを使いこなすことで、冗長な型定義を削減し、変更に強くメンテナンス性の高いコードベースを構築することが可能になります。

まずは小さな定数や設定オブジェクトからtypeofを適用し、型推論の恩恵を感じてみることから始めてみてください。

正確な型定義は、自分自身だけでなく、チーム全体の開発効率とコード品質を飛躍的に高めてくれるはずです。