TypeScript as const とは?型安全性を高める使い方を徹底解説

TypeScript

TypeScript as const とは?型安全性を高める使い方を徹底解説

TypeScript を使っていると「as const」という記法を目にすることがあります。この機能は初心者には理解しにくいかもしれませんが、型安全性を大幅に向上させる強力なツールです。本記事では、as const の基本から実践的な使い方まで、わかりやすく解説します。

as const とは何か:原因の説明

TypeScript の型推論の基本

TypeScript を書くとき、変数に値を代入するとコンパイラが自動的に型を推論します。例えば、以下のコードを見てください。

const message = "Hello";
// message の型は string と推論される

ここで重要なのは、message は「Hello」という具体的な値を持つ変数ですが、TypeScript は型を「string」という広い範囲で推論してしまうということです。これが問題の源です。

as const が解決する問題

実務では、特定の値だけを許可したい場合があります。例えば、ステータスが「success」「error」「loading」の3つだけに限定したいとします。

const status = "success";
// 問題:status は string 型として推論される
// これにより、誤った値を代入してもエラーにならない

status = "invalid"; // 型チェックではエラーにならない

このような場合に「as const」を使うことで、値を「リテラル型」として固定化できます。

const status = "success" as const;
// status の型は "success" に固定される
// より狭く、より具体的な型になる

as const は、TypeScript に「この値は変更しないので、最も狭い型として推論してほしい」と指示する機能なのです。

as const の使い方:解決手順

ステップ1:基本的な使い方を理解する

as const を使う最もシンプルな例から始めましょう。

// as const を使わない場合
const color1 = "red";
// color1 の型:string

// as const を使う場合
const color2 = "red" as const;
// color2 の型:"red" (リテラル型)

このように、as const を付けることで、型が「string」という広い型から「”red”」という狭い型に変わります。

ステップ2:オブジェクトに as const を適用する

単純な値だけでなく、オブジェクトにも as const を使えます。これが非常に便利です。

// as const を使わない場合
const user = {
  name: "Taro",
  age: 25,
  role: "admin"
};
// user の型:{ name: string; age: number; role: string }

// as const を使う場合
const user = {
  name: "Taro",
  age: 25,
  role: "admin"
} as const;
// user の型:{ readonly name: "Taro"; readonly age: 25; readonly role: "admin" }

as const を使うことで、すべてのプロパティが readonly となり、値も具体的なリテラル型で固定されます。

ステップ3:配列に as const を適用する

配列に対しても as const は有効です。

// as const を使わない場合
const colors = ["red", "green", "blue"];
// colors の型:(string)[] 

// as const を使う場合
const colors = ["red", "green", "blue"] as const;
// colors の型:readonly ["red", "green", "blue"]

配列の要素が具体的な値として型に反映されるので、要素の追加や変更を防げます。

コード例:実践的な使用方法

例1:API レスポンスのステータス管理

API の成功・失敗を管理するコードで、as const の威力が発揮されます。

// ステータスの定義
const API_STATUS = {
  SUCCESS: "success",
  ERROR: "error",
  LOADING: "loading"
} as const;

// ステータス型の抽出
type ApiStatus = typeof API_STATUS[keyof typeof API_STATUS];
// ApiStatus = "success" | "error" | "loading"

// 関数での使用
function handleResponse(status: ApiStatus) {
  if (status === "success") {
    console.log("処理成功");
  } else if (status === "error") {
    console.log("エラー発生");
  } else if (status === "loading") {
    console.log("読み込み中");
  }
}

// 正しい使用
handleResponse(API_STATUS.SUCCESS); // OK

// 間違った使用はエラーになる
handleResponse("invalid"); // エラー!

例2:ユーザーロールの制限

// ロール定義
const USER_ROLES = ["admin", "user", "guest"] as const;

// ロール型の抽出
type UserRole = typeof USER_ROLES[number];
// UserRole = "admin" | "user" | "guest"

interface User {
  id: number;
  name: string;
  role: UserRole;
}

const users: User[] = [
  { id: 1, name: "Alice", role: "admin" },
  { id: 2, name: "Bob", role: "user" },
  { id: 3, name: "Charlie", role: "guest" }
];

// 権限チェック関数
function hasPermission(role: UserRole, action: string): boolean {
  const permissions: Record = {
    admin: ["read", "write", "delete"],
    user: ["read", "write"],
    guest: ["read"]
  };
  return permissions[role].includes(action);
}

// 使用例
console.log(hasPermission("admin", "delete")); // true
console.log(hasPermission("guest", "write"));  // false

例3:設定オブジェクトの型安全性

// アプリケーション設定
const APP_CONFIG = {
  env: "production" as const,
  port: 3000 as const,
  debug: false as const,
  features: {
    darkMode: true,
    notifications: true
  } as const
} as const;

// 型の抽出
type AppConfig = typeof APP_CONFIG;
type Environment = typeof APP_CONFIG.env;
// Environment = "production"

// 設定値へのアクセス
const env: Environment = APP_CONFIG.env;

// 誤った変更を防ぐ
// APP_CONFIG.port = 8000; // エラー!readonly

function getConfig(): AppConfig {
  return APP_CONFIG;
}

const config = getConfig();
console.log(config.port); // 3000

よくある間違い

間違い1:as const を忘れて型推論に頼る

// ❌ 間違い
const statusOptions = ["pending", "approved", "rejected"];
// statusOptions の型:string[]
// 型チェックが弱くなり、タイプミスに気づきません

function updateStatus(status: string) {
  // status に "invalid" が渡される可能性がある
}

updateStatus("invalid"); // エラーにならない

// ✅ 正しい
const statusOptions = ["pending", "approved", "rejected"] as const;
// statusOptions の型:readonly ["pending", "approved", "rejected"]
type Status = typeof statusOptions[number];

function updateStatus(status: Status) {
  // Status = "pending" | "approved" | "rejected"
  // のみが許可される
}

updateStatus("invalid"); // エラー!

間違い2:オブジェクトの一部だけに as const を使う

// ❌ 間違い
const config = {
  name: "MyApp" as const,
  version: 1.0, // as const がない
  features: ["auth", "api"] // as const がない
};
// name は "MyApp" 型ですが、version と features は広い型のまま

// ✅ 正しい
const config = {
  name: "MyApp",
  version: 1.0,
  features: ["auth", "api"]
} as const;
// 全てのプロパティが具体的なリテラル型になる

間違い3:let で宣言した変数に as const を使う

// ❌ 間違い(意味がない)
let status = "pending" as const;
status = "approved"; // 値を変更できてしまう
// as const は readonly を保証するが、let では変更可能

// ✅ 正しい
const status = "pending" as const;
// status = "approved"; // エラー!
// const と as const で二重の保護

間違い4:関数の戻り値の型を正しく指定しない

// ❌ 間違い
function getTheme() {
  const theme = { mode: "dark", colors: ["#000", "#fff"] } as const;
  return theme;
}
// 戻り値の型が自動推論されるが、明示的でない

const myTheme = getTheme();
// myTheme の型が不確かになる可能性がある

// ✅ 正しい
const THEME = { mode: "dark", colors: ["#000", "#fff"] } as const;
type Theme = typeof THEME;

function getTheme(): Theme {
  return THEME;
}

const myTheme = getTheme();
// myTheme の型が明確:{ readonly mode: "dark"; readonly colors: readonly ["#000", "#fff"]; }

as const の応用的な活用

Union 型の作成

// ボタンのバリエーションを定義
const BUTTON_VARIANTS = ["primary", "secondary", "danger"] as const;

// Union 型を自動生成
type ButtonVariant = typeof BUTTON_VARIANTS[number];
// ButtonVariant = "primary" | "secondary" | "danger"

interface ButtonProps {
  variant: ButtonVariant;
  label: string;
}

const button: ButtonProps = {
  variant: "primary", // OK
  label: "Click me"
};

// const button2: ButtonProps = {
//   variant: "invalid", // エラー!
//   label: "Click me"
// };

列挙型の代わりとして

// as const で enum の代わりになる
const HTTP_STATUS = {
  OK: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  NOT_FOUND: 404,
  SERVER_ERROR: 500
} as const;

type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
// HttpStatus = 200 | 201 | 400 | 401 | 404 | 500

function handleStatus(code: HttpStatus) {
  switch(code) {
    case HTTP_STATUS.OK:
      console.log("成功");
      break;
    case HTTP_STATUS.NOT_FOUND:
      console.log("見つかりません");
      break;
    default:
      console.log("その他");
  }
}

handleStatus(HTTP_STATUS.OK); // OK
// handleStatus(999); // エラー!

as const 使用時の注意点

パフォーマンスへの影響

as const 自体はコンパイル時に処理されるため、実行時のパフォーマンスに影響はありません。型チェックの厳密さが増すだけです。

互換性の問題

as const は TypeScript 3.4 以降で利用可能です。古いバージョンを使っている場合はアップグレードが必要です。

readonly の理解

as const を使うと、オブジェクトや配列は readonly になります。これは値の変更を防ぐという利点がありますが、既存コードとの互換性を考慮する必要があります。

const config = { name: "App", port: 3000 } as const;

// config.name = "NewApp"; // エラー!readonly
// config.port = 8000; // エラー!readonly

// 値を変更したい場合は新しいオブジェクトを作成
const newConfig = { ...config, port: 8000 };
// newConfig の型は { readonly name: "App"; readonly port: 8000; }

まとめ

TypeScript の as const は、型安全性を大幅に向上させる強力な機能です。

主なポイント:

  • as const は値をリテラル型として固定化する
  • 単純な値、オブジェクト、配列に使用できる
  • 型推論をより厳密にして、タイプミスを防ぐ
  • const と組み合わせることで、値の変更を完全に防ぐ
  • Union 型や enum の代替として活用できる
  • let ではなく const で宣言した時に意味を持つ
  • オブジェクト全体に as const を使うのが推奨される

最初は理解しにくいかもしれませんが、as const を積極的に使うことで、バグの少ないより堅牢なTypeScript コードが書けるようになります。特に、ステータス定義やロール管理、API のレスポンス型定義など、値が固定されるべき場面では as const の使用を心がけましょう。

実際のプロジェクトで使ってみることで、その効果を実感できるはずです。ぜひ今日から as const を活用してください!

タイトルとURLをコピーしました