🌱

TypeScript入門③

に公開

入門 ①では、基本的な型から応用的な機能までを網羅し、入門 ②ではジェネリクスについて深掘りしました。

今回は typeofkeyof、ユーティリティ型、Mapped types、インデックスアクセス型など、型レベルプログラミングに特化した記事です。

型レベルプログラミングとは?

「型レベルプログラミング」とは、「値ではなく型を操作するプログラミング」のことです。通常のプログラミングが変数やデータを使ってロジックを組み立てるのに対し、型レベルプログラミングでは、既存の型を材料にして、新しい型を生成したり、型同士の関係性を定義したりするロジックを組み立てます。

型レベルプログラミングによって以下のようなメリットがあります。

  • 複雑なデータ構造や、動的に変化するデータの型を、より正確かつ厳密に表現できます
  • 既存の型に基づいて、新しい型を自動的に生成できます
  • 「型の汎用性」を高め、より高度で再利用性の高い型や関数を設計できます
  • 型レベルで詳細な情報を持つことで、開発環境(IDE)のコード補完やエラーチェックがさらに賢くなり、開発体験が向上します。

型レベルプログラミングの主要な機能についてみていきましょう。

typeof型演算子

TypeScript における typeof型演算子は、既存の変数やプロパティから、その「型」を抽出して新しい型として利用するための機能です。値から型を逆算することができます。

// 💡 string型の変数
const message = "こんにちは"; // message は string 型と推論されます

// typeof 型演算子を使って、message 変数の型を取得し、新しい型 MessageType を定義
type MessageType = typeof message; // MessageType は string 型になる

keyof型演算子

オブジェクト型から、そのすべてのプロパティ名を文字列リテラルのユニオン型として抽出するための機能です。

// ユーザー情報を表すオブジェクト型を定義
type User = {
  id: number;
  name: string;
  age: number;
};

// 💡 keyof User で User 型のすべてのプロパティ名を取得
type UserKeys = keyof User; // UserKeys は "id" | "name" | "age" となります

let key1: UserKeys = "id"; // ✅ OK
let key2: UserKeys = "name"; // ✅ OK
let key3: UserKeys = "email"; // ❌ エラー:Type '"email"' is not assignable to type '"id" | "name" | "age"'.

ユーティリティ型 (Utility Types)

既存の型を基にして新しい型を作成するためのさまざまな便利な型操作の機能のことです。

Partial<T>: 全てのプロパティをオプショナルに

Partial<T>は、指定した型Tのすべてのプロパティをオプション(任意)にするユーティリティ型です。オブジェクトの一部分だけを持つ型や、更新処理などで一部のプロパティだけを渡したい場合に便利です。

interface User {
  id: number;
  name: string;
  email: string;
}

// 💡 Partial<User> は { id?: number; name?: string; email?: string; } となります
type PartialUser = Partial<User>;

Required<T>: 全てのプロパティを必須に

Required<T>は、Partial<T>とは逆に、指定した型 T のすべてのプロパティを必須(required)にするユーティリティ型です。オプションプロパティを含む型を定義した場合でも、特定の状況ではそのプロパティが必須であることを強制したい場合に役立ちます。

interface Product {
  id: number;
  name: string;
  description?: string; // description はオプション
  price: number;
}

// 💡 Required<Product> は { id: number; name: string; description: string; price: number; } となります
type RequiredProduct = Required<Product>;

Readonly<T>: 全てのプロパティを読み取り専用に

Readonly<T>は、指定した型Tのすべてのプロパティを読み取り専用(readonly)にするユーティリティ型です。データの不変性を確保したい場合に非常に有効です。

interface Config {
  apiUrl: string;
  timeout: number;
}

// 💡 Readonly<Config> は { readonly apiUrl: string; readonly timeout: number; } となります
type ReadonlyConfig = Readonly<Config>;

const appConfig: ReadonlyConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

Record<K, T>: 特定のキーと値の型を持つオブジェクトを作成

Record<K, T>は、特定のキーの型Kと、そのキーに対応する値の型Tを持つオブジェクト型を定義するユーティリティ型です。キーが列挙型や文字列リテラルユニオンで厳密に決まっている場合に非常に便利です。

type Page = "home" | "about" | "contact"; // キーとなる文字列リテラルユニオン型
type PageInfo = { title: string; path: string }; // 値の型

// 💡 Record<Page, PageInfo> は { home: PageInfo; about: PageInfo; contact: PageInfo; } となります
type PageMap = Record<Page, PageInfo>;

const pages: PageMap = {
  home: { title: "ホーム", path: "/" },
  about: { title: "会社概要", path: "/about" },
  contact: { title: "お問い合わせ", path: "/contact" },
}; // ✅ OK

Pick<T, K>: 指定したプロパティだけを抽出

Pick<T, K>は、指定した型Tから、特定のプロパティKだけを「選び出す(pick する)」新しい型を作成するユーティリティ型です。元の型から必要な情報だけを取り出して、よりシンプルで具体的な型を定義したい場合に便利です。

interface UserDetails {
  id: number;
  name: string;
  email: string;
  phone: string;
}

// 💡 Pick<UserDetails, "name" | "email"> は { name: string; email: string; } となります
type UserContact = Pick<UserDetails, "name" | "email">;

const contactInfo: UserContact = {
  name: "Alice",
  email: "alice@example.com",
}; // ✅ OK

Omit<T, K>: 指定したプロパティを除外

Omit<T, K>は、Pick<T, K>とは反対に、指定した型Tから特定のプロパティKを「除外する(omit する)」新しい型を作成するユーティリティ型です。元の型から不要な情報を取り除き、より狭い範囲の型を定義したい場合に便利です。

interface UserProfile {
  id: number;
  name: string;
  passwordHash: string; // パスワードのハッシュ値
  lastLogin: Date;
}

// 💡 Omit<UserProfile, "passwordHash" | "lastLogin"> は { id: number; name: string; } となります
type PublicUserProfile = Omit<UserProfile, "passwordHash" | "lastLogin">;

const publicUser: PublicUserProfile = {
  id: 101,
  name: "Charlie",
}; // ✅ OK

Extract<T, U>: ふたつのユニオン型の共通部分を抽出

Extract<T, U>は、ユニオン型Tから、Uに割り当て可能な型だけを「抽出する(extract する)」新しいユニオン型を作成するユーティリティ型です。Exclude とは逆の動作をします。

type EventType = "click" | "hover" | "submit" | "keypress";
type UIEvent = "click" | "hover";

// 💡 Extract<EventType, UIEvent> は "click" | "hover" となります
type InteractiveEvent = Extract<EventType, UIEvent>;

Exclude<T, U>: ユニオン型から特定の型を除外

Exclude<T, U>は、ユニオン型Tから、Uに割り当て可能な型を「除外する(exclude する)」新しいユニオン型を作成するユーティリティ型です。

type AllColors = "red" | "green" | "blue" | "black" | "white";

// 💡 Exclude<AllColors, "black" | "white"> は "red" | "green" | "blue" となります
type PrimaryColors = Exclude<AllColors, "black" | "white">;

NonNullable<T>: nullまたはundefinedを除外

NonNullable<T>は、型 T からnullまたはundefined型を「除外する(non-nullable にする)」ユーティリティ型です。これにより、値がnullundefinedではないことが保証される型を作成できます。

type PossibleValue = string | number | null | undefined;

// 💡 NonNullable<PossibleValue> は string | number となります
type GuaranteedValue = NonNullable<PossibleValue>;

ReturnType<T>: 関数の戻り値の型を取得

ReturnType<T>は、関数型Tの戻り値の型を取得するユーティリティ型です。既存の関数の戻り値の型を再利用したい場合に非常に便利です。

function getUserData(id: number): { id: number; name: string } {
  return { id: id, name: `User-${id}` };
}

// 💡 ReturnType<typeof getUserData> は { id: number; name: string; } となります
type UserDataType = ReturnType<typeof getUserData>;

Awaited<T>: Promiseの解決型を取得

Awaited<T>は、Promise型を含む型Tが解決された際の(awaitで取り出される)型を取得するユーティリティ型です。非同期処理の結果の型を正確に扱いたい場合に非常に便利です。

type PromiseString = Promise<string>;
type PromiseNumber = Promise<Promise<number>>; // Promiseのネスト

// 💡 Awaited<PromiseString> は string となります
type ResolvedString = Awaited<PromiseString>; // string

// 💡 Awaited<PromiseNumber> は number となります(ネストされたPromiseも解決)
type ResolvedNumber = Awaited<PromiseNumber>; // number

Mapped Types (マップド型)

Mapped Types(マップド型)は、既存のオブジェクト型に基づいて、その各プロパティを「マップ」して新しいオブジェクト型を生成する機能です。
構文としては、[P in K] の形でプロパティをループ処理し、それぞれのプロパティに新しい型を適用します。K は通常、keyof T の結果として得られるプロパティ名のユニオン型が入ります。

// ユーザー情報を表す元の型
interface UserInfo {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// 💡 UserInfoのすべてのプロパティをReadonlyにするMapped Typeの例
//    P は UserInfo の各プロパティ名('id', 'name', 'email', 'isActive')を順に取ります
type ReadonlyUserInfo<T> = {
  readonly [P in keyof T]: T[P]; // 各プロパティPをreadonlyにし、元の型TのプロパティPの型(T[P])を適用
};

type ImmutableUser = ReadonlyUserInfo<UserInfo>;
/* ImmutableUser は以下の型と同じ意味になります:
{
  readonly id: number;
  readonly name: string;
  readonly email: string;
  readonly isActive: boolean;
}
*/

インデックスアクセス型 (Indexed Access Types)

インデックスアクセス型は、オブジェクト型(または配列型)から、特定のプロパティの型を直接「取り出す」ための機能です。オブジェクトの特定のキーに対応する値の型を、その場で取得したい場合に非常に便利です。

構文は Type[Key] の形で記述します。

// ユーザー情報を表す型
type User = {
  id: number;
  name: string;
  email: string;
};

// 💡 User 型の 'name' プロパティの型を取得
type UserNameType = User["name"]; // UserNameType は string 型となります

let userName: UserNameType = "Alice"; // ✅ OK

まとめ

型レベルプログラミングの主要な機能についてまとめました。
これらの機能は、コードの記述量が減るといった表面的なメリットだけでなく、アプリケーションの型安全性を最大限に高め、大規模なプロジェクトでも堅牢性と保守性を維持するために不可欠なツールです。

Discussion