TypeScript as const assertion とは?型推論を固定する使い方と具体例

TypeScript

TypeScript as const assertion とは?型推論を固定する使い方と具体例

TypeScriptを使っていると、配列やオブジェクトの型推論に悩むことがあります。特に定数として定義した値なのに、型が予想外に広くなってしまう場面に遭遇するでしょう。そこで活躍するのが「as const assertion」です。

この記事では、as const assertionの基本から実践的な使い方、よくある間違いまで、初心者向けに丁寧に解説します。

原因の説明:なぜas constが必要なのか

通常の型推論の問題点

TypeScriptで値を定義するとき、明示的に型を指定しなければ、型推論によって自動的に型が決められます。しかし、この自動推論は時に思わぬ結果をもたらします。

例えば、以下のコードを見てください:

const colors = ['red', 'green', 'blue'];

このコードの場合、TypeScriptはcolorsの型をstring[]と推論します。つまり、「任意の文字列の配列」という意味です。しかし、実装者の意図は「赤、緑、青の3色だけに限定した配列」かもしれません。

同様に、オブジェクトでも問題が生じます:

const user = {
  name: 'Taro',
  age: 25
};

この場合、user.nameの型はstringuser.ageの型はnumberと推論されます。後からuser.name = 'Hanako'のように値を変更できる状態です。

as const assertionの役割

as constを使うと、TypeScriptに「この値を変更しない定数として扱い、最も具体的なリテラル型として推論してほしい」と伝えることができます。

リテラル型とは、特定の値だけを許可する型です。例えば、'red'という値だけを許可する型は'red'です。

const colors = ['red', 'green', 'blue'] as const;
// 型は: readonly ['red', 'green', 'blue']

このようにすることで、型チェッカーが「この配列は正確にこれら3つの値だけを持つ」と理解します。

解決手順:as constの使い方

ステップ1:基本的な使い方

as constを使うのは非常に簡単です。値の後ろにas constと書くだけです:

const value = '定数' as const;

ステップ2:配列に適用する

配列全体をリテラル型にしたい場合:

const statuses = ['active', 'inactive', 'pending'] as const;
// 型: readonly ['active', 'inactive', 'pending']

ステップ3:オブジェクトに適用する

オブジェクト全体をリテラル型にしたい場合:

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retryCount: 3
} as const;
// 各プロパティが具体的なリテラル型になる

ステップ4:ネストされた構造に適用する

深くネストされたオブジェクトにも有効です:

const appConfig = {
  database: {
    host: 'localhost',
    port: 5432
  },
  cache: {
    enabled: true,
    ttl: 3600
  }
} as const;

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

例1:ステータス管理

// as constを使わない場合(推奨されない)
const statuses1 = ['pending', 'approved', 'rejected'];
// 型: string[]

// as constを使う場合(推奨)
const statuses2 = ['pending', 'approved', 'rejected'] as const;
// 型: readonly ['pending', 'approved', 'rejected']

// ステータスの型定義
type Status = typeof statuses2[number];
// Type: 'pending' | 'approved' | 'rejected'

// 関数で使用
function handleStatus(status: Status) {
  console.log(`Status: ${status}`);
}

handleStatus('pending'); // OK
handleStatus('approved'); // OK
handleStatus('unknown'); // エラー:型不一致

例2:ロールベースアクセス制御

const roles = ['admin', 'user', 'guest'] as const;

type Role = typeof roles[number];
// Type: 'admin' | 'user' | 'guest'

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

const user: User = {
  id: 1,
  name: 'Taro',
  role: 'admin'
};

user.role = 'superuser'; // エラー:許可された値のみ使用可能

例3:設定オブジェクト

const appSettings = {
  features: {
    darkMode: true,
    notifications: false,
    analytics: true
  },
  limits: {
    maxUploadSize: 10485760, // 10MB
    sessionTimeout: 1800000 // 30min
  }
} as const;

// キーにアクセス
type FeatureKey = keyof typeof appSettings.features;
// Type: 'darkMode' | 'notifications' | 'analytics'

// 値にアクセス
type FeatureValue = typeof appSettings.features[FeatureKey];
// Type: boolean

function toggleFeature(feature: FeatureKey, enabled: boolean) {
  console.log(`Feature ${feature}: ${enabled}`);
}

toggleFeature('darkMode', true); // OK
toggleFeature('invalidFeature', true); // エラー

例4:API レスポンスタイプの定義

const apiEndpoints = {
  users: '/api/users',
  posts: '/api/posts',
  comments: '/api/comments'
} as const;

type Endpoint = typeof apiEndpoints[keyof typeof apiEndpoints];
// Type: '/api/users' | '/api/posts' | '/api/comments'

async function fetchData(endpoint: Endpoint) {
  const response = await fetch(endpoint);
  return response.json();
}

fetchData('/api/users'); // OK
fetchData('/api/invalid'); // エラー

例5:定数列挙体

const colors = {
  RED: '#FF0000',
  GREEN: '#00FF00',
  BLUE: '#0000FF'
} as const;

type ColorCode = typeof colors[keyof typeof colors];
// Type: '#FF0000' | '#00FF00' | '#0000FF'

function applyColor(code: ColorCode) {
  document.body.style.backgroundColor = code;
}

applyColor('#FF0000'); // OK
applyColor('#FFFFFF'); // エラー:定義された色のみ使用可能

よくある間違いと対策

間違い1:readonlyを誤解する

as constを使うと、配列やオブジェクトはreadonlyになります。これは「変更不可」という意味です。

const arr = [1, 2, 3] as const;
arr[0] = 10; // エラー:読み取り専用のため変更不可

arr.push(4); // エラー:配列の操作不可

対策: as constは「定数」を定義するときだけ使用してください。変更が必要な場合は、型注釈で対応します。

// 変更が必要な場合
const arr: (1 | 2 | 3)[] = [1, 2, 3];
arr[0] = 3; // OK

間違い2:配列の要素を誤認識

as constなしで配列を定義したとき、その要素型が予想と異なることがあります。

const tuple1 = [10, 'text', true];
// 型: (number | string | boolean)[]

const tuple2 = [10, 'text', true] as const;
// 型: readonly [10, 'text', true]

// tuple1では要素の型が曖昧
const first: string | number | boolean = tuple1[0]; // 型が広い

// tuple2では要素の型が正確
const first2: 10 = tuple2[0]; // 型が具体的

間違い3:型推論の活用忘れ

as constを使っても、推論結果を活用していない場合があります。

// 悪い例:as constを使っても型を手動で書く
const permissions = ['read', 'write', 'delete'] as const;
type Permission = 'read' | 'write' | 'delete'; // 手動で書くのは冗長

// 良い例:推論結果を活用
const permissions = ['read', 'write', 'delete'] as const;
type Permission = typeof permissions[number]; // 自動で推論

間違い4:オブジェクトのプロパティで部分的に使う

オブジェクト全体にas constを適用しないと、一部のプロパティが期待と異なる型になる可能性があります。

// 部分的な適用は不十分
const config = {
  mode: 'production' as const,
  debug: false
};
// config.debugは boolean のまま(false ではなく)

// 全体に適用
const config2 = {
  mode: 'production',
  debug: false
} as const;
// 両方とも具体的なリテラル型に

間白い5:ジェネリック型での混乱

as constの結果をジェネリック関数に渡すとき、型の変換に注意が必要です。

const items = ['apple', 'banana'] as const;

function process(arr: T): T {
  return arr;
}

const result = process(items);
// resultの型: readonly ['apple', 'banana']
// 元の型情報が保持される

まとめ

as const assertionの要点:

  • 目的: リテラル型を固定し、値の変更を防ぎながら型安全性を向上させる
  • 使い方: 値の後ろに「as const」と記述するだけ
  • 効果: 配列やオブジェクトの型推論が最も具体的になる
  • メリット: 型を手動で定義する手間が減り、保守性が向上
  • 注意点: readonlyになるため、値の変更が不可に

TypeScriptで型安全性を高めたいなら、as constは必須の機能です。特に、複数の箇所で使用される定数値やAPIエンドポイント、設定値などを定義するときに有効です。

初めは「何か難しそう」と感じるかもしれませんが、使っていくうちに「こんなに便利なら手放せない」と感じるようになるでしょう。ぜひプロジェクトで活用してみてください。

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