404 Not Found

404 Not Found


nginx

TypeScriptのジェネリクス(上級編)

ジェネリクスの基礎を習得したところで、このレッスンでは、より高度なトピック――条件付き型、infer、およびマップ型――へと進みます。これらはTypeScriptにおける型ベースのプログラミングの核心となるツールであり、組み込みのユーティリティ型のソースコードを理解するための基礎となります。

1. 条件付き型

条件付き型は、型の条件に基づいて異なる結果を選択します。これは、型レベルにおける三項演算子に似ています:

(1) 基本的な構文

TYPESCRIPT
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 が和集合型の場合、条件付き型は各要素を「分散」した形で個別に評価します:

TYPESCRIPT
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] で囲んでください:

TYPESCRIPT
type IsNever<T> = [T] extends [never] ? "yes" : "no";

type A = IsNever<never>;           // "yes"
type B = IsNever<string | never>;  // "no"(No longer available)

▶ 例:型安全な配列のフラット化

TYPESCRIPT
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) 配列要素の型の推論

TYPESCRIPT
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) 関数の戻り値の型を推測する

TYPESCRIPT
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) 関数のパラメータ型の推論

TYPESCRIPT
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の解決済み値の型の推論

TYPESCRIPT
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) 基本的な構文

TYPESCRIPT
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) マッピング修飾子

修飾子を追加または削除するには、+/- を使用してください:

TYPESCRIPT
// 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 以降)

TYPESCRIPT
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) フィルターのプロパティ

TYPESCRIPT
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クライアントの構築

TYPESCRIPT
// 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. ジェネリックのデフォルト値

ジェネリック型パラメータにはデフォルト値を指定できます。型が推論できない場合や、指定する必要がない場合には、このデフォルト型が使用されます:

TYPESCRIPT
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) デフォルト値に関する制約

デフォルト値を持つ型パラメータは、デフォルト値を持たないパラメータの後に記述する必要があります:

TYPESCRIPT
// ✅ 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>のサブタイプである」—同じ方向への変化:

TYPESCRIPT
// 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)のサブタイプである」――この関係は逆の方向にも成り立ちます:

TYPESCRIPT
// 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) なぜ関数の引数の順序が逆になっているのですか?

TYPESCRIPT
// 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.
💡 要点: 関数の戻り値は共変(より具体的な戻り値型の安全性)であるのに対し、関数の引数は反変(より一般的な引数型の安全性)です。TypeScriptはstrictFunctionTypesモードにおいて、反変チェックを厳格に実施します。


❓ よくある質問

Q infer と汎用型の T の違いは何ですか?
A 汎用型の T は呼び出し元によって指定される型パラメータであるのに対し、infer は条件付き型内で自動的に推論される型変数です。Tは「指定してください」、inferは「自分で推論します」を意味します。inferは、条件付き型のextends節でのみ使用できます。
Q マッピング型とユーティリティ型(Partial や Required など)の関係はどのようなものですか?
A ユーティリティ型は、マッピング型を用いて実装されています。Partial、Required、Readonly、Pick、Omit などは、すべて TypeScript に組み込まれているマッピング型です。マッピング型の仕組みを理解することで、これらのユーティリティ型の動作を理解しやすくなるだけでなく、独自のユーティリティ型を作成することも可能になります。
Q 共変性と反変性は、日常的な開発において重要ですか?
A ほとんどの場合、これらについて考える必要はありません。TypeScriptの型チェックが自動的に処理してくれます。これらを理解する必要があるのは、ジェネリック関数や高階関数、あるいは型ツールを記述するときだけです。初心者は、まずこれらの概念に慣れておき、関数の割り当てで型の不一致に遭遇したときにのみ、より深く掘り下げるようにしてください。
Q 条件付き型はどのような場合に「分散」されるのですか?
A 条件付き型の T が、タプルやオブジェクトなどでラップされていない裸の型パラメータであり、かつユニオン型である場合、評価は分散されます。[T] extends [U] でラップすると、分散は無効になります。IsNeverの例は、分散を無効にする必要がある最も一般的なシナリオです。neverがユニオンの構成要素である場合、分散処理中にneverはスキップされます。

📖 まとめ

📝 練習問題

  1. 基本問題(難易度 ⭐):条件付き型を用いて IsArray<T> を実装してください。T が配列型であれば true を返し、そうでなければ false を返します。IsArray<string[]> および IsArray<number> をテストしてください。
  2. 上級問題(難易度 ⭐⭐):マッピング型を使用して Stringify<T> を実装してください。つまり、オブジェクトのすべてのプロパティの型を string に変換します。例えば、{ age: number }{ age: string } といった具合です。
  3. チャレンジ問題(難易度 ⭐⭐⭐)infer と条件付き型を使用して、DeepPromise<T> を実装してください。つまり、非 Promise 型が得られるまで、ネストされた Promise を再帰的に展開します。テストケース DeepPromise<Promise<Promise<Promise<number>>>> では、数値が返される必要があります。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%