T-CREATOR

高度な TypeScript 型操作:keyof・typeof・infer 演算子の使い方

高度な TypeScript 型操作:keyof・typeof・infer 演算子の使い方

TypeScript の型システムは、JavaScript の動的な性質を型安全に扱うための強力な機能を提供します。特に、keyoftypeofinfer演算子は、型の操作と推論において重要な役割を果たします。本記事では、これらの演算子の基本的な使い方から高度な応用まで、実践的な例を通じて解説していきます。

keyof 演算子

keyof演算子は、オブジェクト型のプロパティ名を型として抽出するために使用されます。これにより、オブジェクトのキーを型安全に扱うことができます。この演算子は、型システム内でオブジェクトのプロパティにアクセスする際の型安全性を確保するために重要な役割を果たします。

基本的な使い方

keyof演算子の基本的な使い方を見ていきましょう。オブジェクト型からプロパティ名のユニオン型を生成することができます。

typescript// 基本的なオブジェクト型のキー抽出
type Person = {
  name: string;
  age: number;
  address: string;
};

type PersonKeys = keyof Person; // "name" | "age" | "address"

// インデックスシグネチャを持つ型のキー抽出
type Dictionary = {
  [key: string]: number;
};

type DictionaryKeys = keyof Dictionary; // string | number

// リテラル型のキー抽出
type Colors = {
  red: '#FF0000';
  green: '#00FF00';
  blue: '#0000FF';
};

type ColorKeys = keyof Colors; // "red" | "green" | "blue"

高度な応用

keyof演算子は、型の変換や制約を定義する際に特に有用です。以下の例では、オブジェクトのプロパティを読み取り専用にしたり、オプショナルにしたりする型を定義しています。

typescript// 読み取り専用プロパティの作成
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyPerson = Readonly<Person>;
// 結果: { readonly name: string; readonly age: number; readonly address: string; }

// オプショナルプロパティの作成
type Partial<T> = {
  [P in keyof T]?: T[P];
};

type PartialPerson = Partial<Person>;
// 結果: { name?: string; age?: number; address?: string; }

// 必須プロパティの作成
type Required<T> = {
  [P in keyof T]-?: T[P];
};

type RequiredPerson = Required<PartialPerson>;
// 結果: { name: string; age: number; address: string; }

実践的なパターン

実際の開発では、keyof演算子を使って、より複雑な型の操作や制約を定義することができます。以下の例では、特定の条件に基づいてプロパティを抽出したり、型を変換したりする方法を示しています。

typescript// null許容プロパティのキーを抽出
type NonNullablePropertyKeys<T> = {
  [K in keyof T]: null extends T[K] ? never : K;
}[keyof T];

type User = {
  name: string;
  age: number | null;
  email: string | null;
};

type RequiredKeys = NonNullablePropertyKeys<User>; // "name"

// 特定の型のプロパティのみを抽出
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type StringProps = PickByType<Person, string>; // { name: string; address: string; }

// プロパティの型を変換
type Transform<T, U> = {
  [K in keyof T]: U;
};

type NumberProps = Transform<Person, number>; // { name: number; age: number; address: number; }

typeof 演算子

typeof演算子は、値から型を生成するために使用されます。これにより、実行時の値の型を型システムで利用することができます。この演算子は、特にリテラル型や関数の型を抽出する際に有用です。

基本的な使い方

typeof演算子を使って、オブジェクトリテラルや関数から型を生成する基本的な例を見ていきましょう。

typescript// オブジェクトリテラルから型を生成
const colors = {
  red: '#FF0000',
  green: '#00FF00',
  blue: '#0000FF',
} as const;

type ColorKeys = keyof typeof colors; // "red" | "green" | "blue"
type ColorValues = (typeof colors)[keyof typeof colors]; // "#FF0000" | "#00FF00" | "#0000FF"

// 関数の型を抽出
function add(a: number, b: number) {
  return a + b;
}

type AddFunction = typeof add; // (a: number, b: number) => number

// クラスの型を抽出
class User {
  constructor(public name: string, public age: number) {}
}

type UserClass = typeof User; // new (name: string, age: number) => User

高度な応用

typeof演算子は、状態管理やイベントハンドラの型を定義する際に特に有用です。以下の例では、状態管理の型生成とイベントハンドラの型生成を示しています。

typescript// 状態管理の型生成
function createState<T>(initialState: T) {
  let state = initialState;
  return {
    getState: () => state,
    setState: (newState: typeof initialState) => {
      state = newState;
    },
  };
}

const userState = createState({ name: 'John', age: 30 });
type UserState = typeof userState;
// 結果: { getState: () => { name: string; age: number; }; setState: (newState: { name: string; age: number; }) => void; }

// イベントハンドラの型生成
const handlers = {
  onClick: (e: MouseEvent) => console.log(e),
  onKeyDown: (e: KeyboardEvent) => console.log(e),
} as const;

type EventHandlers = typeof handlers;
// 結果: { readonly onClick: (e: MouseEvent) => void; readonly onKeyDown: (e: KeyboardEvent) => void; }

実践的なパターン

実際の開発では、typeof演算子を使って、API クライアントやイベントエミッターの型を定義することができます。以下の例では、型安全な API クライアントとイベントエミッターの実装を示しています。

typescript// APIクライアントの型生成
const API = {
  getUser: (id: string) => ({ id, name: 'John' }),
  getPosts: (userId: string) => [
    { id: 1, title: 'Post 1' },
  ],
  createUser: (data: { name: string; age: number }) => ({
    id: '1',
    ...data,
  }),
} as const;

// APIメソッドの型を抽出
type ApiKeys = keyof typeof API;
type ApiReturnTypes = {
  [K in ApiKeys]: ReturnType<(typeof API)[K]>;
};

// 型安全なAPIクライアントの作成
type ApiClient = {
  [K in ApiKeys]: (
    ...args: Parameters<(typeof API)[K]>
  ) => Promise<ReturnType<(typeof API)[K]>>;
};

// 型安全なイベントエミッターの作成
type EventMap = {
  userCreated: { id: string; name: string };
  userDeleted: { id: string };
};

type EventEmitter = {
  on<K extends keyof EventMap>(
    event: K,
    handler: (data: EventMap[K]) => void
  ): void;
  emit<K extends keyof EventMap>(
    event: K,
    data: EventMap[K]
  ): void;
};

infer 演算子

infer演算子は、条件付き型の中で型を抽出するために使用されます。これにより、複雑な型の操作が可能になります。この演算子は、特に関数の戻り値の型や配列の要素の型を抽出する際に有用です。

基本的な使い方

infer演算子を使って、関数の戻り値の型や配列の要素の型を抽出する基本的な例を見ていきましょう。

typescript// 関数の戻り値の型を抽出
type ExtractReturnType<T> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

type FunctionReturnType = ExtractReturnType<
  typeof API.getUser
>;
// 結果: { id: string; name: string; }

// 配列の要素の型を抽出
type ArrayElement<T> = T extends Array<infer U> ? U : never;

type Numbers = number[];
type NumberElement = ArrayElement<Numbers>; // number

// Promiseの解決値の型を抽出
type PromiseValue<T> = T extends Promise<infer U>
  ? U
  : never;

type AsyncResult = Promise<string>;
type Result = PromiseValue<AsyncResult>; // string

高度な応用

infer演算子は、再帰的な型定義や条件付き型の抽出に特に有用です。以下の例では、再帰的な型定義と条件付き型の抽出を示しています。

typescript// 再帰的な型定義
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

type NestedObject = {
  name: string;
  details: {
    age: number;
    address: {
      city: string;
      country: string;
    };
  };
};

type ReadonlyNested = DeepReadonly<NestedObject>;
// 結果: { readonly name: string; readonly details: { readonly age: number; readonly address: { readonly city: string; readonly country: string; }; }; }

// 条件付き型の抽出
type ExtractType<T, U> = T extends U ? T : never;

type StringOrNumber = string | number;
type OnlyString = ExtractType<string, StringOrNumber>; // string
type OnlyNumber = ExtractType<number, StringOrNumber>; // number

実践的なパターン

実際の開発では、infer演算子を使って、型の変換や合成を行うことができます。以下の例では、型の変換と合成の実践例を示しています。

typescript// 型の変換と抽出
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type AsyncFunction = () => Promise<string>;
type SyncFunction = () => string;

type UnwrappedAsync = UnwrapPromise<
  ReturnType<AsyncFunction>
>; // string
type UnwrappedSync = UnwrapPromise<
  ReturnType<SyncFunction>
>; // string

// 型の合成
type Merge<T, U> = {
  [K in keyof T | keyof U]: K extends keyof U
    ? U[K]
    : K extends keyof T
    ? T[K]
    : never;
};

type A = { a: string; b: number };
type B = { b: string; c: boolean };

type Merged = Merge<A, B>; // { a: string; b: string; c: boolean }

// 型の制約と検証
type Validate<T, U> = T extends U ? T : never;

type StringOrNumber = string | number;
type OnlyString = Validate<string, StringOrNumber>; // string
type OnlyNumber = Validate<number, StringOrNumber>; // number

実践的な型操作パターン

型の合成と分解

型の合成と分解は、複雑な型を扱う際に重要なパターンです。以下の例では、型の合成と分解の実践例を示しています。

typescript// 型の合成
type Merge<T, U> = {
  [K in keyof T | keyof U]: K extends keyof U
    ? U[K]
    : K extends keyof T
    ? T[K]
    : never;
};

type A = { a: string; b: number };
type B = { b: string; c: boolean };

type Merged = Merge<A, B>; // { a: string; b: string; c: boolean }

// 型の分解
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type Omit<T, K extends keyof T> = Pick<
  T,
  Exclude<keyof T, K>
>;

type User = {
  id: string;
  name: string;
  email: string;
  password: string;
};

type PublicUser = Omit<User, 'password'>; // { id: string; name: string; email: string; }

型の変換とマッピング

型の変換とマッピングは、型を別の型に変換する際に有用なパターンです。以下の例では、型の変換とマッピングの実践例を示しています。

typescript// 型の変換
type MapToFunction<T> = {
  [K in keyof T]: (value: T[K]) => void;
};

type Config = {
  name: string;
  age: number;
  isActive: boolean;
};

type ConfigHandlers = MapToFunction<Config>;
// 結果: { name: (value: string) => void; age: (value: number) => void; isActive: (value: boolean) => void; }

// 型のマッピング
type MapToNullable<T> = {
  [K in keyof T]: T[K] | null;
};

type NullableConfig = MapToNullable<Config>;
// 結果: { name: string | null; age: number | null; isActive: boolean | null; }

型の制約と検証

型の制約と検証は、型の安全性を確保するために重要なパターンです。以下の例では、型の制約と検証の実践例を示しています。

typescript// 型の制約
type Validate<T, U> = T extends U ? T : never;

type StringOrNumber = string | number;
type OnlyString = Validate<string, StringOrNumber>; // string
type OnlyNumber = Validate<number, StringOrNumber>; // number

// 型の検証
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<123>; // false

// 型の条件分岐
type ConditionalType<T> = T extends string
  ? { type: 'string'; value: T }
  : T extends number
  ? { type: 'number'; value: T }
  : { type: 'unknown'; value: T };

type StringResult = ConditionalType<'hello'>; // { type: "string"; value: "hello"; }
type NumberResult = ConditionalType<123>; // { type: "number"; value: 123; }

まとめ

TypeScript の高度な型操作は、型安全なコードを書くための強力なツールです。keyoftypeofinfer演算子を組み合わせることで、複雑な型の操作や推論が可能になります。これらの演算子を理解し、適切に活用することで、より堅牢な TypeScript コードを書くことができます。

関連リンク