TypeScriptのジェネリクス(上級編)
ジェネリクスの基礎を習得したところで、このレッスンでは、より高度なトピック――条件付き型、infer、およびマップ型――へと進みます。これらはTypeScriptにおける型ベースのプログラミングの核心となるツールであり、組み込みのユーティリティ型のソースコードを理解するための基礎となります。
1. 条件付き型
条件付き型は、型の条件に基づいて異なる結果を選択します。これは、型レベルにおける三項演算子に似ています:
(1) 基本的な構文
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes" ("hello" is a string subtype of)
(2) 条件付き型と和集合型(分散)
T が和集合型の場合、条件付き型は各要素を「分散」した形で個別に評価します:
type ToString<T> = T extends string ? "string" : "other";
// T = string | number → Evaluate each one separately → "string" | "other"
type Result = ToString<string | number>; // "string" | "other"
(3) 分散動作を無効にする
分散評価を無効にするには、コードを [T] で囲んでください:
type IsNever<T> = [T] extends [never] ? "yes" : "no";
type A = IsNever<never>; // "yes"
type B = IsNever<string | never>; // "no"(No longer available)
▶ 例:型安全な配列のフラット化
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number
type C = Flatten<boolean[]>; // boolean
2. infer キーワード
infer 型条件における型変数の「推論」――これはTypeScriptの型プログラミングにおいて最も強力なツールの一つです:
(1) 配列要素の型の推論
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type A = ArrayElement<string[]>; // string
type B = ArrayElement<number[]>; // number
type C = ArrayElement<string>; // never(string Not an array)
(2) 関数の戻り値の型を推測する
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type A = GetReturnType<() => string>; // string
type B = GetReturnType<(x: number) => boolean>; // boolean
type C = GetReturnType<(x: string) => void>; // void
type D = GetReturnType<string>; // never(string Not a function)
(3) 関数のパラメータ型の推論
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;
type A = GetParameters<(a: string, b: number) => void>; // [string, number]
type B = GetParameters<() => void>; // []
type C = GetParameters<(x: boolean) => string>; // [boolean]
(4) Promiseの解決済み値の型の推論
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<number[]>>; // number[]
type C = Awaited<string>; // string(No Promise,Return directly)
// nested Promise——Recursive Unpacking
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type D = DeepAwaited<Promise<Promise<string>>>; // string
3. マップ型
マッピングタイプ:既存のタイプに基づいて新しいタイプを作成する—各プロパティに変換を適用する:
(1) 基本的な構文
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }
type OptionalUser = Optional<User>;
// { name?: string; age?: number; email?: string }
(2) マッピング修飾子
修飾子を追加または削除するには、+/- を使用してください:
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Remove (optional)(?)
type Required2<T> = {
[K in keyof T]-?: T[K];
};
interface Config {
readonly host: string;
readonly port: number;
debug?: boolean;
}
type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean } —— readonly Removed
type RequiredConfig = Required2<Config>;
// { readonly host: string; readonly port: number; debug: boolean } —— ? Removed
(3) キーの再割り当て(TypeScript 4.1 以降)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
(4) フィルターのプロパティ
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string } —— Keep only string Type Properties
▶ 例:型安全なAPIクライアントの構築
// Definition API Routing Type
type ApiRoutes = {
"/users": { response: { id: number; name: string }[] };
"/users/:id": { response: { id: number; name: string }; params: { id: number } };
"/posts": { response: { id: number; title: string }[] };
};
// Extract Response Type Based on Route
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];
type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]
type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }
// Type-safe fetch Function
function fetchApi<R extends keyof ApiRoutes>(
route: R,
...args: "params" extends keyof ApiRoutes[R]
? [params: ApiRoutes[R]["params"]]
: []
): Promise<ApiResponse<R>> {
// Implementation Omitted
return {} as any;
}
// Automatically infer parameters and return types at runtime
// let users = fetchApi("/users"); // No parameters required
// let user = fetchApi("/users/:id", { id: 1 }); // Must be provided params
4. ジェネリックのデフォルト値
ジェネリック型パラメータにはデフォルト値を指定できます。型が推論できない場合や、指定する必要がない場合には、このデフォルト型が使用されます:
interface PaginatedResult<T, PageSize = 10> {
data: T[];
total: number;
pageSize: PageSize;
}
// Use the default values
type UserResult = PaginatedResult<User>;
// data: User[]; total: number; pageSize: 10
// Override the default value
type CustomResult = PaginatedResult<User, 20>;
// data: User[]; total: number; pageSize: 20
(1) デフォルト値に関する制約
デフォルト値を持つ型パラメータは、デフォルト値を持たないパラメータの後に記述する必要があります:
// ✅ Correct
type A<T, U = string> = { first: T; second: U };
// ❌ Error——Those with default values U When there is no default value, T Previous
// type B<U = string, T> = { first: T; second: U };
5. 共分散と不変性
これは、TypeScriptの型システムにおいて最も理解が難しい高度な概念の一つです。ある代入が有効で、別の代入が無効である理由を理解するには、この概念を把握しておく必要があります。
(1) 共変
「AがBのサブタイプであるならば、Container<A>もContainer<B>のサブタイプである」—同じ方向への変化:
// string is a subtype of string | number
// string[] Me too (string | number)[] subtypes of → Covariation
let strings: string[] = ["a", "b"];
let mixed: (string | number)[] = strings; // ✅ Covariant Safety
(2) 随伴(反変)
関数のパラメータ型は逆の関係にあります――「AがBのサブタイプであるならば、(B => void)は(A => void)のサブタイプである」――この関係は逆の方向にも成り立ちます:
// string is a subtype of string | number
// (string | number => void) is a subtype of (string => void) → Inversion
type StringHandler = (arg: string) => void;
type MixedHandler = (arg: string | number) => void;
let mixedHandler: MixedHandler = (arg) => console.log(arg);
let stringHandler: StringHandler = mixedHandler; // ✅ Inversion——Functions that handle broader types can be assigned to variables that handle narrower types.
(3) なぜ関数の引数の順序が逆になっているのですか?
// If the parameter is covariant——Unsafe
let dogHandler: (dog: Dog) => void = (dog) => dog.bark();
let animalHandler: (animal: Animal) => void = dogHandler; // ❌ Danger!
// Call animalHandler(cat) → dog.bark() called on cat → Runtime Error
// Inverting is the safe option——Functions that handle broader types can safely handle narrower types.
strictFunctionTypesモードにおいて、反変チェックを厳格に実施します。
❓ よくある質問
infer と汎用型の T の違いは何ですか?T は呼び出し元によって指定される型パラメータであるのに対し、infer は条件付き型内で自動的に推論される型変数です。Tは「指定してください」、inferは「自分で推論します」を意味します。inferは、条件付き型のextends節でのみ使用できます。[T] extends [U] でラップすると、分散は無効になります。IsNeverの例は、分散を無効にする必要がある最も一般的なシナリオです。neverがユニオンの構成要素である場合、分散処理中にneverはスキップされます。📖 まとめ
- 条件型
T extends U ? X : Yは、型レベルにおける三項演算子である - 推論:条件付き型における型変数を推論する――配列の要素、関数の戻り値、および Promise の解決値を推論する
- 既存のタイプを基に新しいタイプを作成するマッピングタイプ—
{ [K in keyof T]: T[K] }—は、+/- 修飾子およびキーの再割り当てに対応しています - ジェネリックのデフォルト値
<T = DefaultValue>ジェネリックパラメータのフォールバック型を指定する - 共変性(同じ方向)と反変性(反対の方向)は、ジェネリック型のサブタイプ間の関係を表す。関数の戻り値の型は共変であり、パラメータは反変である。
📝 練習問題
- 基本問題(難易度 ⭐):条件付き型を用いて
IsArray<T>を実装してください。T が配列型であれば true を返し、そうでなければ false を返します。IsArray<string[]>およびIsArray<number>をテストしてください。 - 上級問題(難易度 ⭐⭐):マッピング型を使用して
Stringify<T>を実装してください。つまり、オブジェクトのすべてのプロパティの型をstringに変換します。例えば、{ age: number }→{ age: string }といった具合です。 - チャレンジ問題(難易度 ⭐⭐⭐):
inferと条件付き型を使用して、DeepPromise<T>を実装してください。つまり、非 Promise 型が得られるまで、ネストされた Promise を再帰的に展開します。テストケースDeepPromise<Promise<Promise<Promise<number>>>>では、数値が返される必要があります。



