TypeScriptの基礎 - ユーティリティ型(応用)
概要
応用的なユーティリティ型とは、TypeScriptが標準で提供するジェネリック型のうち、条件型 (Conditional Types) と infer キーワードを基盤として構築された、
より高度な型操作を行うための型である。
基本的なユーティリティ型 (Partial、Required、Readonly 等) がオブジェクト型のプロパティを変換するのに対し、
応用的なユーティリティ型は関数の戻り値型や引数型の抽出、Promiseが解決した後の型の取得、ユニオン型のフィルタリング等を実現する。
これらのユーティリティ型を活用することにより、ライブラリや外部モジュールの型定義から必要な型情報を安全に抽出できる。
型を明示的に再定義する必要がなくなり、型定義の変更に追随した保守性の高いコードを記述できる。
基本的なユーティリティ型の詳細については、TypeScriptの基礎 - ユーティリティ型(基本)のページを参照すること。
| ユーティリティ型 | 概要 |
|---|---|
ReturnType<T> |
関数型 T の戻り値の型を取得する
|
Parameters<T> |
関数型 T の引数の型をタプルとして取得する
|
Awaited<T> |
Promise 型 T が解決された後の型を再帰的に取得する
|
Extract<T, U> |
ユニオン型 T から型 U に代入可能な型のみを抽出する
|
Exclude<T, U> |
ユニオン型 T から型 U に代入可能な型を除外する
|
NonNullable<T> |
型 T から null と undefined を除外する
|
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;
null と undefined はそのまま返し、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から null と undefined を除外するユーティリティ型である。
Exclude<T, U> の特殊ケースに相当し、null安全なコードを記述する場合に頻繁に使用する。
内部定義
NonNullable<T> の内部定義を以下に示す。
type NonNullable<T> = T extends null | undefined ? never : T;
条件型により、null と undefined のみを 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() は型レベルのみで動作し、ランタイムには影響しない。
実際のモジュールのインポートは行われないため、バンドルサイズに影響しない点がメリットである。
関連情報