📘

【イラスト付き】発展的な型操作【丁寧に解説】

2024/09/05に公開

はじめに

皆さんこんにちは。
今回はTypeScriptでの発展的な型操作についてご紹介します。

TypeScriptは型から新たな型を生成することができます。型の一部を取得したり、似たような型を生成するなど、型を再利用して新たな型を生成できます。今回はTypeScriptでの型の再利用についてご紹介します。

こんな人にオススメ

  • TypeScriptの発展的な型操作について知りたい。
  • TypeScriptのユーティリティ型について知りたい。

初めて学習する方にも分かるように、丁寧に解説していきます。
すでに利用されている方も、是非一度目を通していただけると嬉しいです。

😋 TypeScriptでの発展的な型操作について紹介します♪

keyof型演算子

まずポイントをチェック

  • オブジェクトの型定義からプロパティ名を型として取り出す
  • プロパティが複数ある場合はユニオン型として取り出す
  • Mapped Typesと併用される

keyofはオブジェクトの型定義からプロパティ名を型として取り出します。プロパティの値の型ではなく、プロパティ名自体を型として取り出します。

プロパティが複数ある場合は、ユニオン型として取り出します。

TypeScript/07.advanced-usage/app.ts(keyofのみ抜粋)
// keyof
type Book = { title: string, price: number };
type bookKeys = keyof Book; // title | price のユニオン型
const bookKey: bookKeys = 'title';

😋 オブジェクトのプロパティ名を型として取り出せます♪

インデックスアクセス型

まずポイントをチェック

  • オブジェクトのプロパティから型を取り出す
  • 複数プロパティをユニオン型で取得可能
  • 配列の要素からも型を取り出し可能

インデックスアクセス型は、オブジェクト型から個別のプロパティの型を取り出します。

オブジェクト型から「[]」でプロパティ名を指定し特定のプロパティを型を取得します。keyofと組み合わせることで全てのプロパティの型を取り出すこともできます。その際はユニオン型で取得できます。

TypeScript/07.advanced-usage/app.ts(オブジェクトのプロパティの型の取得のみ抜粋)
// インデックスアクセス型
// オブジェクトのプロパティの型を取り出す
type Person = { name: string, age: number };
type nameType = Person['name']; // string
// 全てのプロパティの型を取り出す
type personTypes = Person[keyof Person]; // string | number

ちなみに配列の要素からも型を取得することができます。添字を指定して型を取得します。

TypeScript/07.advanced-usage/app.ts(全てのプロパティ型の取得のみ抜粋)

// 配列の要素の型を取り出す
type Tuple = [string, number];
type firstType = Tuple[0]; // string
type secondType = Tuple[1]; // number

😋 オブジェクトのプロパティの型を取り出せます♪

Mapped Types(マップド型)

まずポイントをチェック

  • オブジェクト型を生成できる
  • 既存のオブジェクト型を元に新しいオブジェクト型を生成できる

Mapped Typesはオブジェクトの型を生成するための機能です。既存のオブジェクト型から新しいオブジェクト型を作ることができます。

Mapped Typesはユニオン型の各型を繰り返し処理し、プロパティとして定義していきます。

書式は「[K in ユニオン型] : プロパティの型」でプロパティを表しています。ユニオン型の型の件数分繰り返し、プロパティを定義します。Kはユニオン型から取り出した型名が入る一時的な変数です。

下記はシンプルな例です。繰り返しの基準となるユニオン型に「’key1′ | ‘key2’」が指定されているので、key1とkey2プロパティを持つオブジェクト型になります。それぞれの型は今回は固定でstring型を指定しています。

TypeScript/07.advanced-usage/app.ts(シンプルなMapped Typesのみ抜粋)
// key1とkey2プロパティを持つオブジェクト型を生成する
type Simple = {
    [K in 'key1' | 'key2']: string
};
type StringKeys = Simple;
// StringKeys型の内容
// {
//     key1: string;
//     key2: string;
// }

既存のオブジェクト型からkeyofでプロパティ名のユニオン型を取得することで、動的にオブジェクト型を生成することができます。また、各プロパティの型はインデックスアクセス型を利用し、既存のオブジェクトから再利用することができます。

下記はジェネリクスを利用し、渡されたオブジェクト型のプロパティを全て省略可能にしたオブジェクト型を生成する例です。

「keyof T」でTに渡されたオブジェクトからプロパティをユニオン型で取り出します。それを繰り返してプロパティを生成します。「?」を置くことで生成したプロパティを省略可能にしています。

プロパティの型は「T[K]」となっています。Tは渡されたオブジェクト型であるためインデックスアクセス型で元のプロパティの型を取得し、それを生成したプロパティの型にそのまま利用しています。

Book型を渡した場合は、繰り返しの中でBook[name]の様に動作します。これによりプロパティの型は元のオブジェクト型と同じになります。

Book型はtitleとpriceの2つのプロパティを必須としたオブジェクト型ですが、これを元にどちらのプロパティも省略可能な新しい型を生成しています。

TypeScript/07.advanced-usage/app.ts(既存のオブジェクト型から新しいオブジェクト型を生成のみ抜粋)

type Optional<T> = {
    [K in keyof T]?: T[K]
};
// Book型のプロパティを全て省略可能にする
type BookOptional = Optional<Book>;
// priceプロパティを省略しても代入できる
const book2: BookOptional = { title: 'ABC' };
// BookOptional型の内容
// {
//     title?: string | undefined;
//     price?: number | undefined;
// }

😋 既存のオブジェクト型から新しいオブジェクト型を生成します♪

Conditional Types(条件付き型)

まずポイントをチェック

  • 条件によって型を導き出す
  • 条件演算子を利用する

Conditional Typesは条件式によって型を生成する型操作です。

「T extends 型 ? trueの場合 : falseの場合」のように記述します。ジェネリクスで与えられたT型がextendsで指定された型を満たすかを判定します。

下記の例はTがstring型であればtrue型(リテラル型)を返します。

TypeScript/07.advanced-usage/app.ts(シンプルな例のみ抜粋)

// Conditional Types
// string型か判断し、true型かfalse型を取得
type IsString<T> = T extends string ? true : false;
type checkString = IsString<'Hello'>; // true型

extensdの後ろがオブジェクト型の場合、全てのプロパティを持つかを判断します。

下記の例は、T型がBook型を満たすかを判断しています。満たさない場合はエラーの意味合いで代入不可能なnever型を返します。満たす場合はT型をそのまま利用しています。

TypeScript/07.advanced-usage/app.ts(オブジェクトの例のみ抜粋)
// Book型を満たすか判断し、Book型かnever型を取得
type extendsBook<T> = T extends Book ? T : never;
type checkBook = extendsBook<Book>; // Bookを満たす
const book1: checkBook = { title: 'ABC', price: 500 };

😋 指定の型が条件を満たすか判断します♪

infer

まずポイントをチェック

  • Conditional Typesでextendsと組み合わせて使うキーワード
  • 既存の型から一部の型を取り出す
    • 関数型の戻り値の型
    • 関数型の引数の型
    • 配列型の個別の要素の型

inferは型の一部を取得するために使います。関数型の戻り値型のみを取得するなど、既存の型の一部を取得することができます。

inferはConditional Typesの中でextendsと合わせて利用します。取得したい型の前にinferキーワードを指定します。

関数型の戻り値の型を取得

inferで戻り値型を指定します。

下記例では二つの関数showとcalcから戻り値型を取得しています。GetRTypeのextendsの後ろの「(…args: any[]) => infer R」は関数型を表しており、T型が関数の場合trueと評価されます。「infer R」で戻り値型を取得するよう指示をしています。

TypeScript/07.advanced-usage/app.ts(関数型の戻り値の型を取得のみ抜粋)

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

// 関数型の定義
type show = (name: string) => string; // 戻り値stringの関数
type calc = (x: number, y: number) => number; // 戻り値numberの関数

// 戻り値の型を取得
type showRType = GetRType<show>; // string
type calcRtype = GetRType<calc>; // number

関数型の引数の型を取得

inferで引数の型を指定します。

下記例では二つの関数showとcalcから引数の型を取得しています。GetArgTypeのextendsの後ろの「(…args: infer A) => any」は関数型を表しており、T型が関数の場合trueと評価されます。「infer A」で引数の型を取得するよう指示をしています。

なお、引数の型を取得する際はタプル型として取得されます。

TypeScript/07.advanced-usage/app.ts(関数型の引数の型を取得のみ抜粋)

// 関数型の引数の型を取得
type GetArgType<T> = T extends (...args: infer A) => any ? A : never;

// 引数の型を取得(タプル型で取得される)
type showArgType = GetArgType<show>; // [name: string]
type calcArgType = GetArgType<calc>; // [x: number, y: number]
const showArg: showArgType = ['abc'];
const calcArg: calcArgType = [1, 2];

配列型の個別の要素の型

配列の個別の要素の型を取得するにはinferで配列の要素型を指定します。

下記例では三つの配列Array1,2,3から要素の型を取得しています。GetElementTypeのextendsの後ろの「(infer U)[]」は配列型を表しており、T型が配列の場合trueと評価されます。「infer U」で要素の型を取得するよう指示をしています。

タプル型や要素がユニオン型の場合は、ユニオン型で複数の型が取得されます。

TypeScript/07.advanced-usage/app.ts(配列の要素の型を取得のみ抜粋)

// 配列型の個別の要素の型を取得
type GetElementType<T> = T extends (infer U)[] ? U : never;

// 配列型の定義
type Array1 = [number, string, boolean]; // タプル型
type Array2 = (number | string)[]; // ユニオン型の配列
type Array3 = string[]; // string型の配列

// ElementType を使用して個別の要素の型を取得
type Array1Types = GetElementType<Array1>; // number | string | boolean
type Array2Types = GetElementType<Array2>; // number | string 
type Array3Types = GetElementType<Array3>; // string

😋 条件判断で関数や配列の型の一部を取り出します♪

Utility Type

まずポイントをチェック

  • 既存の型から別の型を取得する型
  • 便利な型がある程度用意されている
    • Partial オブジェクト型の全てのプロパティをオプションにする
    • ReturnType 関数型の戻り値の型を取得
    • Parameters 関数型の引数の型を取得

TypeScriptにはあらかじめ複雑な型操作が可能な型が用意されています。

代表的なものを三つご紹介します。もちろん他にも便利な型操作が用意されています。

Partialはオブジェクト型の全てのプロパティを省略可能にします。Mapped Typesを活用した型操作です。

TypeScript/07.advanced-usage/app.ts(Partialのみ抜粋)

// 指定された型のすべてのプロパティを省略可能にする型を生成
type OptionalType = Partial<Book>;
// {
//     title?: string | undefined;
//     price?: number | undefined;
// }

ReturnTypeは関数型の戻り値の型を取得します。Conditional Typesを活用した型操作です。

TypeScript/07.advanced-usage/app.ts(ReturnTypeのみ抜粋)

// 関数型からその戻り値型を取得
type RType = ReturnType<show>; // string

Parametersは関数型の引数の型を取得します。Conditional Typesを活用した型操作です。

TypeScript/07.advanced-usage/app.ts(Parametersのみ抜粋)

// 関数型からその引数型のタプル型を取得
type ArgType = Parameters<show>; // [name: string]

😋 標準で様々な型操作の機能が用意されています♪

おわりに

皆さん、お疲れ様でした。
ここまでご覧いただき、ありがとうございました。

TypeScriptでの発展的な型の扱いについて確認をしていただきました。
TypeScriptでは型から新たな型を作ることができます。条件判断のような通常のプログラミングでできることを型にも当てはめることができます。
通常このような型操作を使うことは少ないですが、ライブラリの内部などではこのような発展的な型操作が行われています。

😋 これからもプログラミング学習頑張りましょう♪

参考リンク集
サンプルコード

Discussion