TypeScript 速習チュートリアル

TypeScript 高度な型

1. 高度なTypeScriptの型システム

TypeScriptの高度な型システムを使用すると、複雑な型の関係を精密にモデル化できます。これらの機能は、優れた型安全性を備えた、堅牢でメンテナンス性の高いアプリケーションを構築する際に特に有用です。

主な高度な型の機能:

  • Mapped Types: 既存の型のプロパティを変換する
  • Conditional Types: 条件に基づいて型を作成する
  • Template Literal Types: 文字列テンプレートを使用して型を構築する
  • Utility Types: 一般的な変換のための組み込み型ヘルパー
  • Recursive Types: ツリー構造などのための自己参照型
  • Type Guards & Type Predicates: 実行時の型チェック
  • Type Inference: infer を使用した高度なパターンマッチング

2. Mapped Types(マップ型)

Mapped Typesを使用すると、既存の型のプロパティを変換して新しい型を作成できます。

2.1 基本的な Mapped Type

単一のテンプレートを使用して、オブジェクト型のすべてのプロパティを新しい型に変換します。

例:

// すべてのプロパティを boolean に変換
type Flags<T> = {
  [K in keyof T]: boolean;
};

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

type UserFlags = Flags<User>;
// 以下と同等:
// {
//   id: boolean;
//   name: boolean;
//   email: boolean;
// }

2.2 Mapped Type の修飾子(Modifiers)

すべてのキーに対して、readonly?(オプショナル)などのプロパティ修飾子を追加または削除できます。

例:

// すべてのプロパティをオプショナルにする
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type OptionalTodo = {
  [K in keyof Todo]?: Todo[K];
};

// 'readonly' と '?' 修飾子を削除する
type Concrete<T> = {
  -readonly [K in keyof T]-?: T[K];
};

// すべてのプロパティに 'readonly' と 'required' を追加する
type ReadonlyRequired<T> = {
  +readonly [K in keyof T]-?: T[K];
};

2.3 キーの再マッピング(Key Remapping)

as キーワード、文字列ヘルパー、条件チェックを使用して、マッピング中にキーの名前を変更したりフィルタリングしたりできます。

例:

// すべてのプロパティ名にプレフィックス(接頭辞)を追加
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
// }

// 関数であるプロパティのみを抽出(フィルタリング)
type MethodsOnly<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

3. Conditional Types(条件付き型)

Conditional Typesを使用すると、条件に応じて変化する型を定義できます。

3.1 基本的な Conditional Types

型レベルでチェックされる条件に基づいて、型を選択します。

例:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;        // true
type B = IsString<number>;        // false
type C = IsString<'hello'>;       // true
type D = IsString<string | number>; // boolean

// 配列の要素の型を抽出
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = ArrayElement<number[]>; // number

3.2 infer キーワード

Conditional Typesの中で infer を使用して新しい型変数を導入することで、型の一部をキャプチャできます。

例:

// 関数の戻り値の型を取得
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

// 引数の型をタプルとして取得
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// コンストラクタの引数の型を取得
type ConstructorParameters<T extends new (...args: any) => any> =
  T extends new (...args: infer P) => any ? P : never;

// コンストラクタからインスタンスの型を取得
type InstanceType<T extends new (...args: any) => any> =
  T extends new (...args: any) => infer R ? R : any;

3.3 分散的な Conditional Types(Distributed Conditional Types)

ユニオン型に対して条件がどのように分配されるか、あるいは分配を避けるためにどのようにラップするかを理解することが重要です。

例:

// 分配されないケース(配列としてラップ)
type ToArrayNonDist<T> = T extends any ? T[] : never;
type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[]

// 分配されるケース
type ToArray<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr2 = ToArray<string | number>; // string[] | number[]

// 文字列以外の型をフィルタリングして除外
type FilterStrings<T> = T extends string ? T : never;
type Letters = FilterStrings<'a' | 'b' | 1 | 2 | 'c'>; // 'a' | 'b' | 'c'

4. Template Literal Types(テンプレートリテラル型)

Template Literal Typesを使用すると、テンプレートリテラルの構文を使って型を構築できます。

4.1 基本的な Template Literal Types

テンプレートリテラルとユニオン型を使用して、文字列を特定のパターンに制限します。

例:

type Greeting = `Hello, ${string}`;

const validGreeting: Greeting = 'Hello, World!';
const invalidGreeting: Greeting = 'Hi there!'; // エラー

// ユニオン型との組み合わせ
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';

type Style = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | 'red-large' |
// 'green-small' | 'green-medium' | 'green-large' |
// 'blue-small' | 'blue-medium' | 'blue-large'

4.2 文字列操作型

組み込みのヘルパーを使用して、文字列リテラル型を変換(大文字化、先頭大文字化など)します。

例:

// 組み込みの文字列操作型
type T1 = Uppercase<'hello'>;     // 'HELLO'
type T2 = Lowercase<'WORLD'>;     // 'world'
type T3 = Capitalize<'typescript'>; // 'Typescript'
type T4 = Uncapitalize<'TypeScript'>; // 'typeScript'

// イベントハンドラの型を作成
type EventType = 'click' | 'change' | 'keydown';
type EventHandler = `on${Capitalize<EventType>}`;
// 'onClick' | 'onChange' | 'onKeydown'

4.3 高度なパターン

テンプレート、推論(inference)、キーの再マッピングを組み合わせて、メタデータを抽出したりAPIを生成したりします。

例:

// ルートパラメータを抽出
type ExtractRouteParams<T> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<`${Rest}`>]: string }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string; }

// 型安全なイベントエミッターを作成
type EventMap = {
  click: { x: number; y: number };
  change: string;
  keydown: { key: string; code: number };
};

type EventHandlers = {
  [K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void;
};

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

TypeScriptは、一般的な型の変換のために、いくつかの組み込みユーティリティ型を提供しています。

5.1 一般的な Utility Types

PartialPickOmit などの組み込み型を使用して、一般的な変換を行います。

例:

// 基本となる型
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// すべてのプロパティをオプショナルにする
type PartialUser = Partial<User>;

// すべてのプロパティを必須にする
type RequiredUser = Required<PartialUser>;

// すべてのプロパティを読み取り専用にする
type ReadonlyUser = Readonly<User>;

// 特定のプロパティのみを選択する
type UserPreview = Pick<User, 'id' | 'name'>;

// 特定のプロパティを除外する
type UserWithoutEmail = Omit<User, 'email'>;

// プロパティの型を抽出する
type UserId = User['id']; // number
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'createdAt'

5.2 高度な Utility Types

ユニオン型から特定のメンバーを除外または抽出したり、カスタムのマップドヘルパーを作成したりします。

例:

// null と undefined を除外する型を作成
type NonNullable<T> = T extends null | undefined ? never : T;

// ユニオン型から特定の型を除外
type Numbers = 1 | 2 | 3 | 'a' | 'b';
type JustNumbers = Exclude<Numbers, string>; // 1 | 2 | 3

// ユニオン型から特定の型を抽出
type JustStrings = Extract<Numbers, string>; // 'a' | 'b'

// 2番目の型に含まれないプロパティを持つ型を取得
type A = { a: string; b: number; c: boolean };
type B = { a: string; b: number };
type C = Omit<A, keyof B>; // { c: boolean }

// すべてのプロパティをミュータブル(変更可能)にする型を作成
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

6. Recursive Types(再帰的な型)

再帰的な型は、型が自身を参照できるため、ツリー状のデータ構造をモデル化するのに便利です。

6.1 基本的な Recursive Type

ツリーやネストされたJSONのような自己参照構造をモデル化します。

例:

// シンプルな二分木
type BinaryTree<T> = {
  value: T;
  left?: BinaryTree<T>;
  right?: BinaryTree<T>;
};

// JSON風のデータ構造
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// ネストされたコメント
type Comment = {
  id: number;
  content: string;
  replies: Comment[];
  createdAt: Date;
};

6.2 高度な Recursive Types

連結リスト、ディレクトリツリー、再帰的な状態マシンを表現します。

例:

// 連結リスト(Linked List)の型
type LinkedList<T> = {
  value: T;
  next: LinkedList<T> | null;
};

// ディレクトリ構造の型
type File = {
  type: 'file';
  name: string;
  size: number;
};

type Directory = {
  type: 'directory';
  name: string;
  children: (File | Directory)[];
};

// 状態マシンの型
type State = {
  value: string;
  transitions: {
    [event: string]: State;
  };
};

// 再帰関数の型
type RecursiveFunction<T> = (x: T | RecursiveFunction<T>) => void;

7. ベストプラクティス

7.1 高度な型をいつ使うべきか

  • オブジェクト型の複数のプロパティを変換する必要がある場合は、Mapped Types を使用します。
  • 型が別の型に依存する場合は、Conditional Types を使用します。
  • 文字列の操作やパターンマッチングには、Template Literal Types を使用します。
  • 一般的な変換には、Utility Types を使用します(可能な限り組み込みのものを優先してください)。
  • ツリー構造やネストされたデータ構造には、Recursive Types を使用します。

7.2 パフォーマンスに関する考慮事項

  • 深くネストされた再帰的な型は、TypeScriptコンパイラの動作を遅くする可能性があります。
  • 非常に大きなユニオン型(100メンバー以上)は、パフォーマンスの問題を引き起こす可能性があります。
  • 複雑な型を分解するために、型エイリアス(Type Alias)を適切に使用してください。

7.3 よくある落とし穴

  • 型推論の問題: Conditional Typesはユニオン型に対して分散されるため、予期しない動作をすることがあります。
  • infer の動作: infer を使った型推論は、コンテキストによって動作が異なる場合があります。
  • Utility Types の制限: 一部のユーティリティ型は、anyunknown に対して期待通りに動作しないことがあります。
  • メンテナンス性: 高度な型を使いすぎると、コードの理解が困難になります。複雑な型変換にはコメントでドキュメントを残してください。非常に複雑な型については、型アサーションやヘルパー関数の使用を検討してください。