TypeScriptの基礎 - ユーティリティ型(応用)

提供: MochiuWiki : SUSE, EC, PCB

概要

応用的なユーティリティ型とは、TypeScriptが標準で提供するジェネリック型のうち、条件型 (Conditional Types) と infer キーワードを基盤として構築された、
より高度な型操作を行うための型である。

基本的なユーティリティ型 (PartialRequiredReadonly 等) がオブジェクト型のプロパティを変換するのに対し、
応用的なユーティリティ型は関数の戻り値型や引数型の抽出、Promiseが解決した後の型の取得、ユニオン型のフィルタリング等を実現する。

これらのユーティリティ型を活用することにより、ライブラリや外部モジュールの型定義から必要な型情報を安全に抽出できる。
型を明示的に再定義する必要がなくなり、型定義の変更に追随した保守性の高いコードを記述できる。

基本的なユーティリティ型の詳細については、TypeScriptの基礎 - ユーティリティ型(基本)のページを参照すること。

応用ユーティリティ型の一覧
ユーティリティ型 概要
ReturnType<T> 関数型 T の戻り値の型を取得する
Parameters<T> 関数型 T の引数の型をタプルとして取得する
Awaited<T> PromiseT が解決された後の型を再帰的に取得する
Extract<T, U> ユニオン型 T から型 U に代入可能な型のみを抽出する
Exclude<T, U> ユニオン型 T から型 U に代入可能な型を除外する
NonNullable<T> T から nullundefined を除外する



ReturnType<T>

ReturnType<T> は、関数型 T の戻り値の型を取得するユーティリティ型である。

関数の実装を変更した際に、戻り値の型を別途更新する必要がなくなり、型定義の一元管理が可能になる。

内部定義

ReturnType<T> の内部定義を以下に示す。

 type ReturnType<T extends (...args: any) => any> =
    T extends (...args: any) => infer R ? R : any;


T extends (...args: any) => infer R という条件型により、関数型の戻り値の部分を infer R でキャプチャする。

条件が成立する場合は R が、成立しない場合は any が返される。

実用例

typeof 演算子と組み合わせることにより、既存の関数から型情報を抽出できる。

 function getUser(id: string) {
    return { id, name: "Alice", email: "alice@example.com" };
 }
 
 type User = ReturnType<typeof getUser>;
 // { id: string; name: string; email: string }


ジェネリック関数の戻り値型も取得できる。

 function createArray<T>(item: T): T[] {
    return [item];
 }
 
 type StringArray = ReturnType<typeof createArray<string>>;
 // string[]


非同期関数に ReturnType<T> を適用すると Promise<T> が返るため、解決後の型を得るには後述の Awaited<T> と組み合わせる必要がある。


Parameters<T>

Parameters<T> は、関数型Tの引数の型をタプル型として取得するユーティリティ型である。

関数の引数型を再利用してラッパー関数を定義する際に有用であり、元の関数の引数型の変更に追随できる。

内部定義

Parameters<T> の内部定義を以下に示す。

 type Parameters<T extends (...args: any) => any> =
    T extends (...args: infer P) => any ? P : never;


infer P により関数の引数リスト全体をタプル型としてキャプチャする。
関数型でない場合は never が返される。

実用例

Parameters<T>ReturnType<T> を組み合わせることにより、元の関数と同じシグネチャを持つラッパー関数を型安全に作成できる。

以下の例では、loggedAdd(a: number, b: number) => number 型として推論されるため、型安全にラッパー関数を定義できる。

 function createLogger<T extends (...args: any[]) => any>(fn: T) {
    return function logged(...args: Parameters<T>): ReturnType<T> {
       console.log(`Calling with args:`, args);
       const result = fn(...args);
       console.log(`Result:`, result);
       return result;
    };
 }
 
 const add = (a: number, b: number) => a + b;
 const loggedAdd = createLogger(add);
 loggedAdd(5, 3);  // 型安全に呼び出せる



Awaited<T>

Awaited<T> は、Promise 型が解決された後の型を再帰的に取得するユーティリティ型である。

TypeScript 4.5で導入されており、ネストした Promise 型も再帰的に解決できる。

内部定義

Awaited<T> の内部定義を以下に示す。

 type Awaited<T> =
    T extends null | undefined ? T :
    T extends object & { then(onfulfilled: infer F): any } ?
       F extends (value: infer V) => any ? Awaited<V> : never :
    T;


nullundefined はそのまま返し、thenableオブジェクト (Promiseライクなオブジェクト) に対しては、then メソッドのコールバック引数型を再帰的に解決する。

基本的な使用例

Awaited<T> の基本的な挙動を以下に示す。

 type A = Awaited<Promise<string>>;           // string
 type B = Awaited<Promise<Promise<number>>>;  // number (再帰的に解決)
 type C = Awaited<string | Promise<number>>;  // string | number


ReturnType<T>との組み合わせ

非同期関数の戻り値型を取得する場合は、ReturnType<T>Awaited<T> を組み合わせて使用する。

 async function getUser(id: string) {
    return { id, name: "Alice" };
 }
 
 // ReturnType単体ではPromise<{ id: string; name: string }>になる
 type UserPromise = ReturnType<typeof getUser>;
 // Promise<{ id: string; name: string }>
 
 // Awaitedと組み合わせることでPromiseを解決した型を得る
 type User = Awaited<ReturnType<typeof getUser>>;
 // { id: string; name: string }


Awaited<ReturnType<typeof <関数名>>> というパターンは、非同期関数の解決後の型を取得する時の定番の組み合わせである。


Extract<T, U>

Extract<T, U> は、ユニオン型 T から型 U に代入可能な型のみを抽出するユーティリティ型である。
ユニオン型から特定の型のサブセットを取り出す場合に使用する。

内部定義

Extract<T, U> の内部定義を以下に示す。

 type Extract<T, U> = T extends U ? T : never;


分配条件型 (Distributive Conditional Types) の性質により、ユニオン型の各メンバーに対して条件型が個別に適用される。
U に代入可能な型は残り、代入不可能な型は never となって消去される。

実用例

  • イベント型のユニオンから特定のイベントのみを抽出する例
     type EventType = "click" | "scroll" | "resize" | "keydown" | "keyup";
     
     type KeyboardEvents = Extract<EventType, "keydown" | "keyup">;
     // "keydown" | "keyup"
     
     type MouseEvents = Extract<EventType, "click" | "scroll" | "resize">;
     // "click" | "scroll" | "resize"
    

  • 関数型のみを抽出する例
    Extract<T, U>U に代入可能であればよいため、スーパータイプとの比較も可能である。
     type T = Extract<string | number | (() => void), Function>;
     // () => void
    



Exclude<T, U>

Exclude<T, U> は、ユニオン型 T から型 U に代入可能な型を除外するユーティリティ型である。
Extract<T, U> と逆の操作を行い、特定の型をユニオンから取り除く場合に使用する。

内部定義

Exclude<T, U> の内部定義を以下に示す。

 type Exclude<T, U> = T extends U ? never : T;


分配条件型により各メンバーに個別に適用され、U に代入可能な型は never となって消去され、代入不可能な型のみが残る。

実用例

  • ユニオン型から特定の値を除外する例
     type T0 = Exclude<"a" | "b" | "c", "a" | "c">;
     // "b"
    

  • イベント型のユニオンからキーボードイベントを除外する例
     type AllEvents = "click" | "scroll" | "resize" | "keydown" | "keyup";
     
     type NonKeyboardEvents = Exclude<AllEvents, "keydown" | "keyup">;
     // "click" | "scroll" | "resize"
    


Extract<T, U>Exclude<T, U> の使い分けとして、
残したい型が明確な場合は Extract<T, U> を、除外したい型が明確な場合は Exclude<T, U> を使用するとよい。


NonNullable<T>

NonNullable<T> は、型Tから nullundefined を除外するユーティリティ型である。
Exclude<T, U> の特殊ケースに相当し、null安全なコードを記述する場合に頻繁に使用する。

内部定義

NonNullable<T> の内部定義を以下に示す。

 type NonNullable<T> = T extends null | undefined ? never : T;


条件型により、nullundefined のみを never に変換して除外する。

基本的な使用例

NonNullable<T> の基本的な挙動を以下に示す。

 type T0 = NonNullable<string | number | undefined>;
 // string | number
 
 type T1 = NonNullable<(() => string) | string[] | null | undefined>;
 // (() => string) | string[]


型ガード関数との組み合わせ

NonNullable<T> は型ガード関数と組み合わせることにより、配列から null / undefined を型安全に除去できる。

 function isDefined<T>(value: T | null | undefined): value is NonNullable<T> {
    return value !== null && value !== undefined;
 }
 
 const values: (string | number | null | undefined)[] = ["a", 1, null, undefined, "b"];
 const definedValues = values.filter(isDefined);
 // definedValuesの型 : (string | number)[]


上記の型ガード関数isDefinedは、value is NonNullable<T> という型述語を持つため、filter() メソッドに渡した時にフィルタ後の配列の型が正確に推論される。

型ガード関数の詳細については、TypeScriptの基礎 - 型の絞り込みのページを参照すること。


ライブラリの型定義を読むパターン

TypeScriptプロジェクトでは、ライブラリの型定義ファイル (@types パッケージ) を読み解き、既存の型定義を活用することが保守性の向上につながる。

@types パッケージは node_modules/@types/ ディレクトリ以下に配置されており、DefinitelyTypedコミュニティによってメンテナンスされている。

React.ComponentPropsの活用

Reactを使用するプロジェクトでは、React.ComponentProps<T> を用いてコンポーネントやDOM要素のProps型を抽出できる。

  • コンポーネントのProps型を抽出する例
     import React from "react";
     
     // コンポーネントのProps型を抽出
     type ButtonProps = React.ComponentProps<typeof Button>;
     
     // DOM要素のProps型を抽出
     type DivProps   = React.ComponentProps<"div">;
     type InputProps = React.ComponentProps<"input">;
    

  • 抽出したProps型を拡張してカスタムコンポーネントを定義する例
     // DOM要素を拡張したカスタムコンポーネント
     const CustomInput: React.FC<InputProps & { label: string }> = ({
        label,
        ...inputProps
     }) => (
        <div>
           <label>{label}</label>
           <input {...inputProps} />
        </div>
     );
    

  • コンポーネントの特定のProps属性のみを抽出する例
     type IconNameProp = React.ComponentProps<typeof Icon>["name"];
     // "home" | "settings" | "user" | "logout"
    


なお、ComponentProps<T> の関連型として、以下に示すバリアントがある。

  • React.ComponentPropsWithRef<T>
    ref プロップを含むProps型を取得する。
    forwardRef を使用するコンポーネントに使用する。
  • React.ComponentPropsWithoutRef<T>
    ref プロップを除いたProps型を取得する。
    ref 転送が不要なコンポーネントに使用する。


typeof import()を使用したモジュール型の動的取得

typeof import() を使用することにより、モジュール全体の型をランタイムインポートなしに取得できる。

 // モジュール型の動的取得
 type UtilsModule       = typeof import("./utils");
 type CalculateFunction = UtilsModule["calculate"];


typeof import() は型レベルのみで動作し、ランタイムには影響しない。

実際のモジュールのインポートは行われないため、バンドルサイズに影響しない点がメリットである。


関連情報