TypeScript

TypeScriptのkeyof・typeof・as const:定数から型を作る実践ガイド

この記事でわかること

TypeScriptのkeyof・typeof・as constを使い、定数からキーや値の型を作る方法を解説。ファイル別のコード例で、型安全なプロパティ取得、配列からのユニオン型、readonlyと実行時の違いを学べます。

設定値と型定義を別々に管理すると、値を追加したのに型を更新し忘れることがあります。TypeScriptのtypeofで値の型を取り出し、keyofでキーの一覧を型にし、as constでリテラル型を保持すると、ひとつの定義から必要な型を作れます。

この記事では、記事の公開状態と表示ラベルを題材に、定数から型を導出する方法を紹介します。掲載コードはTypeScript 4.9.4のstrictモードで型チェックと実行確認をしています。基本的なオブジェクト型を知っている方を対象にしています。

1. keyofは「型のキー」を取り出す

keyofは、オブジェクト型が持つキーの型を取り出す演算子です。たとえば、記事を表す型から、指定できるプロパティ名だけを取り出せます。

ファイル:src/examples/keyof-basics.ts

type Article = {
  id: string;
  title: string;
  views: number;
};

type ArticleKey = keyof Article;
// "id" | "title" | "views"

const selectedKey: ArticleKey = "title";

ArticleKeyは単なるstringではないため、"body"のような未定義のキーを代入すると型エラーになります。プロパティ名を引数に受け取る関数などで役立ちます。

ただし、keyofが常に少数の文字列リテラルになるわけではありません。文字列のインデックスシグネチャを持つ型ではstring | numberになる場合があり、シンボルをキーに持つ型ではシンボルの型も含まれます。この記事では固定の文字列キーを持つオブジェクトを扱います。

参考:TypeScript Handbook — Keyof Type Operator

2. typeofで値から型を取り出す

すでに設定オブジェクトがある場合は、同じ構造の型を手書きする代わりに、型の位置でtypeofを使えます。

ファイル:src/examples/typeof-basics.ts

const settings = {
  pageSize: 12,
  showAuthor: true,
};

type Settings = typeof settings;
// { pageSize: number; showAuthor: boolean }

const compactSettings: Settings = {
  pageSize: 6,
  showAuthor: false,
};

ここで使うtypeof settingsは、実行時に"object"などを返すJavaScriptのtypeofとは役割が異なります。型の位置では、変数に推論された型を参照します。上の例ではpageSizenumberなので、別の数値を設定できます。

型を取り出しても、値の複製や実行時の検証は行われません。APIの応答の正しさを確かめたい場合は、別途バリデーションが必要です。

参考:TypeScript Handbook — Typeof Type Operator

3. keyof typeofas constで状態名とラベルを一元管理する

次は記事の状態と表示ラベルを、ひとつの定数で管理します。ファイルを分けておけば、型と値を同じ場所から読み込めます。

ファイル:src/lib/article-status.ts

export const articleStatus = {
  draft: "下書き",
  published: "公開中",
  archived: "アーカイブ",
} as const;

export type ArticleStatus = keyof typeof articleStatus;
// "draft" | "published" | "archived"

export type ArticleStatusLabel =
  (typeof articleStatus)[ArticleStatus];
// "下書き" | "公開中" | "アーカイブ"

export function getStatusLabel(
  status: ArticleStatus,
): ArticleStatusLabel {
  return articleStatus[status];
}

keyof typeof articleStatusは、まずtypeofで定数の型を取り出し、その型にkeyofを適用しています。状態名を文字列のユニオンとして別に書き直す必要がありません。

(typeof articleStatus)[ArticleStatus]はインデックスアクセス型です。指定したキーに対応する値の型を取り出し、この例では3つのラベルのユニオンになります。

as constを付けると、このオブジェクトリテラルのプロパティは読み取り専用の型として扱われ、値もstringへ広がらず、具体的な文字列リテラル型として推論されます。なお、固定キーの一覧を作るだけなら、as constがなくてもkeyof typeofは使えます。

参考:TypeScript Handbook — Indexed Access TypesTypeScript 3.4 — const assertions

4. 配列の要素からユニオン型を作る

候補を順番どおりに表示したい場合は、オブジェクトではなく配列を元にすると扱いやすくなります。

ファイル:src/lib/article-order.ts

export const articleOrders = ["newest", "oldest"] as const;

export type ArticleOrder = (typeof articleOrders)[number];
// "newest" | "oldest"

export function getOrderLabel(order: ArticleOrder): string {
  return order === "newest" ? "新しい順" : "古い順";
}

as constによって、articleOrdersは読み取り専用のタプル型になります。[number]は、配列やタプルの数値インデックスに対応する要素の型を取り出す指定です。

articleOrders.map(...)を使えば、この定数を画面の選択肢にも使えます。候補と型を別々に管理するより、変更箇所をまとめやすくなります。

5. keyofとジェネリクスで型安全にプロパティを取得する

オブジェクトから任意のプロパティを取り出す関数では、キーだけでなく戻り値の型も連動させられます。

ファイル:src/lib/get-property.ts

export function getProperty<T, K extends keyof T>(
  object: T,
  key: K,
): T[K] {
  return object[key];
}

K extends keyof Tは、引数のキーをそのオブジェクトに存在するキーへ制限します。戻り値をT[K]にすることで、指定したキーの値の型が返ります。

ファイル:src/examples/article-status.ts

import { getStatusLabel } from "../lib/article-status";
import { articleOrders, getOrderLabel } from "../lib/article-order";
import { getProperty } from "../lib/get-property";

const article = {
  id: "post-1",
  title: "定数から型を作る",
  views: 120,
};

const title = getProperty(article, "title"); // string
const views = getProperty(article, "views"); // number

console.log(title.toUpperCase());
console.log(views.toFixed(0));
console.log(getStatusLabel("published")); // 公開中
console.log(articleOrders.map(getOrderLabel)); // ["新しい順", "古い順"]

getProperty(article, "missing")は、存在しないキーなので型エラーになります。また、引数のキー自体がユニオン型なら、戻り値も対応する値のユニオン型になることがあります。「常に単一の型になる」と考えず、渡すキーの型にも注目してください。

6. as constでできないことを押さえる

オブジェクトを実行時に凍結する機能ではない

as constはTypeScriptの型の仕組みです。JavaScriptを実行するときにオブジェクトを凍結する処理は追加されません。

ファイル:src/examples/const-reference.ts

const tags = ["typescript"];
const articleMeta = { tags } as const;

// tagsは既存の可変配列を参照しているため、要素を追加できる
articleMeta.tags.push("nextjs");
console.log(articleMeta.tags); // ["typescript", "nextjs"]

この例ではarticleMeta.tagsというプロパティの再代入は制限されますが、参照先の配列は可変のままです。外部で作った値まで、再帰的にすべて変更不可能になるわけではありません。

実行時の変更を防ぐためにObject.freeze()を使う場合も、通常は浅い凍結であることに注意してください。内部のオブジェクトや配列まで凍結したい場合は、その構造に応じた処理が必要です。

Object.keysの戻り値をそのままkeyofとして扱わない

TypeScript 4.9.4の標準の宣言では、Object.keys(articleStatus)の戻り値の型はstring[]です。(keyof typeof articleStatus)[]とは推論されません。実行時のオブジェクトが型に書かれたもの以外のプロパティを持つこともあるためです。

安易な型アサーションでエラーを消す前に、その場所でキーの集合を保証できるかを確認しましょう。選択肢の順序と候補を管理したいだけなら、前の節のように読み取り専用タプルを定義する方法もあります。

7. ファイルを作って型チェックする

この記事の例は次の構成で保存できます。

src/
  lib/
    article-status.ts
    article-order.ts
    get-property.ts
  examples/
    keyof-basics.ts
    typeof-basics.ts
    article-status.ts
    const-reference.ts

TypeScriptがインストール済みのプロジェクトで、次のコマンドを実行すると、すべての例を厳密な設定で型チェックできます。

npx tsc --strict --noEmit --target ES2020 --module commonjs --skipLibCheck src/examples/keyof-basics.ts src/examples/typeof-basics.ts src/examples/article-status.ts src/examples/const-reference.ts

ファイルを明示したこのコマンドでは、tsconfig.jsonの設定は読み込まれず、コマンドラインの指定が使われます。既存プロジェクト全体を普段の設定で確認したい場合は、tsconfig.jsonのあるディレクトリでnpx tsc --noEmitを実行してください。

よくある疑問

keyoftypeofはどう使い分けますか?

typeofは値から型を取り出すとき、keyofは型からキーの型を取り出すときに使います。設定オブジェクトのキーを使いたければ、keyof typeof settingsのように組み合わせます。

as constはすべての変数に付けるべきですか?

いいえ。固定の候補や変更しない設定など、具体的な値を型に残したい場面で使います。後から内容を更新する状態には、必要に応じて通常の型注釈を使ってください。

APIの応答もtypeofだけで検証できますか?

型の位置のtypeofだけでは、実行時のデータ検証はできません。APIの応答はunknownで受け取り、必要な項目や値を実行時に検証してから使います。

関連する開発ノート

y.
WRITTEN BY

y_ymo10

SEの部屋で、TypeScript・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →