TypeScriptの基礎 - ジェネリクスの制約

提供: MochiuWiki : SUSE, EC, PCB

概要

ジェネリクスの制約 (Generic Constraints) とは、型パラメータが受け入れる型の範囲を制限する仕組みである。

ジェネリクスの基本的な用途については、TypeScriptの基礎 - ジェネリクスの基本のページを参照すること。

型パラメータに制約を設けない場合、その型パラメータは任意の型を受け入れるため、コンパイラはその型がどのようなプロパティやメソッドを持つかを知ることができない。
制約を与えることで、型パラメータが特定のプロパティやメソッドを持つことを保証でき、型安全なコードを記述できる。

下表に、ジェネリクスの制約に関連する主要な構文を示す。

これらの構文を組み合わせることにより、柔軟かつ型安全な汎用コードを記述できる。

ジェネリクスの制約に関連する主要な構文
構文 概要
T extends SomeType 型パラメータTをSomeTypeに制約する。
K extends keyof T 型パラメータKをTのプロパティ名のユニオン型に制約する。
T[K] インデックスアクセス型
T型のKプロパティの型を取得する。
T extends U ? X : Y 条件型
TがUに代入可能な場合はX、そうでない場合はYとなる。
infer R 条件型の中で型を推論するキーワード
{ [K in keyof T]: ... } マップ型
Tの各プロパティに変換を適用する。
as マップ型でのキーのリマッピングに使用する。



extendsによる型制約

extends キーワードを使用することで、型パラメータが特定の型の部分型であることを要求できる。
制約を設けることにより、型パラメータが持つプロパティやメソッドへの安全なアクセスが可能となる。

基本的な構文

T extends SomeType という構文で、型パラメータTをSomeTypeに制約する。
SomeTypeに代入可能な型のみがTとして受け入れられる。

string型に制約した型パラメータの例を以下に示す。

 function processString<T extends string>(value: T): T {
    return value;
 }
 
 processString("hello");  // OK : string型
 // processString(123);   // エラー : numberはstringに代入できない


オブジェクト型による制約

オブジェクト型の構造を制約として使用することができる。
例えば、{ length: number } のように構造を直接記述することにより、length プロパティを持つ型のみを受け入れるよう制約できる。

string型と配列型はいずれも length プロパティを持つため、以下の例ではどちらも受け入れられる。

 function findLongest<T extends { length: number }>(a: T, b: T): T {
    return a.length > b.length ? a : b;
 }
 
 findLongest("hello", "world");   // OK : string型はlengthプロパティを持つ
 findLongest([1, 2, 3], [1, 2]);  // OK : 配列型はlengthプロパティを持つ
 // findLongest(1, 2);            // エラー : numberはlengthプロパティを持たない


インターフェースを制約として使用した例を以下に示す。

 interface Named { name: string; }
 interface Aged  { age: number; }
 
 function getPersonInfo<T extends Named & Aged>(person: T): string {
    return `${person.name} is ${person.age} years old`;
 }


複数の制約

交差型 (&) を使用することで、複数のインターフェースや型を同時に制約として指定できる。
型パラメータは全ての制約を満たす型でなければならない。

以下の例では、SerializableLoggable の両方のインターフェースを実装した型のみが、Tとして受け入れられる。

 interface Serializable { serialize(): string; }
 interface Loggable    { log(): void; }
 
 function process<T extends Serializable & Loggable>(item: T): void {
    item.log();
    const serialized = item.serialize();
    console.log(serialized);
 }



keyofとの組み合わせ

keyof 演算子はオブジェクト型のプロパティ名をユニオン型として取得する。
ジェネリクスと組み合わせることにより、型安全なプロパティアクセスが実現できる。

keyof演算子の基本

keyof T と記述することで、型Tが持つ全てのプロパティ名のユニオン型を取得できる。

 type Person = { name: string; age: number; email: string; };
 type PersonKeys = keyof Person;  // "name" | "age" | "email"


インデックスアクセス型 T[K] を使用することにより、型TのKプロパティが持つ型を取得できる。

 type Person = { name: string; age: number; };
 
 type NameType  = Person["name"];        // string
 type AgeType   = Person["age"];         // number
 type ValueType = Person[keyof Person];  // string | number


ジェネリクスとkeyofの活用

K extends keyof T という制約を使用することにより、型Tに実際に存在するプロパティ名のみをKとして受け入れるよう制約できる。

戻り値の型に T[K] を指定することで、プロパティの型と戻り値の型が一致することをコンパイラが保証する。

型安全なプロパティ取得関数の例を以下に示す。

 function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
 }
 
 const person = { name: "Bob", age: 25 };
 const name = getProperty(person, "name");  // 型 : string
 const age  = getProperty(person, "age");   // 型 : number
 // getProperty(person, "email");           // エラー : "email"はkeyof typeof personにない


同様に、型安全なプロパティ設定関数も実装できる。
引数 value の型を T[K] とすることで、プロパティの型と一致する値のみを受け入れる。

 function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
    obj[key] = value;
 }
 
 const person = { name: "Alice", age: 30 };
 setProperty(person, "name", "Bob");   // OK
 // setProperty(person, "age", "30");  // エラー : stringはnumberに代入できない


複数のプロパティを選択して新しいオブジェクトを作成する pick 関数の例を以下に示す。

 function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
    const result = {} as Pick<T, K>;
    keys.forEach(key => { result[key] = obj[key]; });
    return result;
 }
 
 const person = { name: "Alice", age: 30, email: "alice@example.com" };
 const picked = pick(person, "name", "age");
 // 型 : { name: string; age: number; }



条件型 (Conditional Types)

条件型は、型レベルでの条件分岐を実現する構文である。
型パラメータの種類によって異なる型を返すユーティリティ型の実装に広く使用される。

基本的な構文

条件型の基本構文は、T extends U ? X : Y である。
TがUに代入可能な場合はX型、そうでない場合はY型になる。

 type IsString<T> = T extends string ? true : false;
 
 type A = IsString<"hello">;  // true
 type B = IsString<number>;   // false
 type C = IsString<string>;   // true


条件型を使用した型フィルタリングの例を以下に示す。

 // Tがnull または undefinedの場合に、neverを返す
 type NonNullable<T> = T extends null | undefined ? never : T;
 
 type A = NonNullable<string | null>;       // string
 type B = NonNullable<number | undefined>;  // number


inferキーワード

infer キーワードは、条件型の extends 節の中で使用し、型の一部を推論して変数として取り出すために使用する。
例えば、infer R と記述することで、その位置にある型をRとして取り出し、条件型のthen側 (? 以降) で使用できる。

以下に、infer キーワードを使用した代表的なパターンを示す。

 // 関数の戻り値型を推論
 type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 
 type Fn = () => string;
 type R = MyReturnType<Fn>;  // string


 // 配列の要素型を推論
 type ArrayElement<T> = T extends (infer E)[] ? E : never;
 
 type Numbers = ArrayElement<number[]>;  // number
 type Strings = ArrayElement<string[]>;  // string


 // Promise の解決型を推論
 type MyAwaited<T> = T extends Promise<infer U> ? U : T;
 
 type Resolved = MyAwaited<Promise<string>>;  // string
 type Plain    = MyAwaited<number>;           // number


 // タプルの最後の要素を取得
 type Last<T extends any[]> = T extends [...any[], infer U] ? U : never;
 
 type L1 = Last<[1, 2, 3]>;         // 3
 type L2 = Last<[string, number]>;  // number


分配条件型 (Distributive Conditional Types)

型パラメータが裸の型パラメータ (Naked Type Parameter) である条件型にユニオン型を渡すと、ユニオンの各メンバーに対して条件型が個別に適用される。
この動作を分配条件型 (Distributive Conditional Types) と呼ぶ。

以下の例では、ToArray<string | number>ToArray<string> | ToArray<number> として評価される。

 type ToArray<T> = T extends any ? T[] : never;
 
 type Result = ToArray<string | number>;  // string[] | number[]


分配を防ぎたい場合は、型パラメータをタプル [T] で囲む。
タプルで囲むことにより、ユニオン型が分配されずに全体として評価される。

 type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
 
 type Result = ToArrayNonDist<string | number>;  // (string | number)[]



マップ型 (Mapped Types)

マップ型は、既存の型の全てのプロパティに対して変換を適用することで新しい型を生成する構文である。

TypeScriptの組み込みユーティリティ型の多くは、マップ型を使用して定義されている。

基本的な構文

マップ型の基本構文は { [K in keyof T]: ... } である。
K in keyof T はTの全てのプロパティ名をKとして順に参照して、各プロパティの型変換を定義する。

全プロパティを null 許容にするマップ型の例を以下に示す。

 type Nullable<T> = { [K in keyof T]: T[K] | null; };
 
 interface Person { name: string; age: number; }
 type NullablePerson = Nullable<Person>;
 // { name: string | null; age: number | null; }


全プロパティを読み取り専用 (readonly) にするマップ型の例を以下に示す。

 type ReadOnly<T> = { readonly [K in keyof T]: T[K]; };
 
 interface Config { host: string; port: number; }
 type ReadOnlyConfig = ReadOnly<Config>;
 // { readonly host: string; readonly port: number; }


マッピング修飾子

マップ型では、readonly? (オプショナル) の修飾子を追加または削除できる。

+ を付けると修飾子を追加して、- を付けると修飾子を削除する。
+ は省略可能であり、+readonlyreadonly は同等である。

下表に、マッピング修飾子の一覧を示す。

マッピング修飾子の一覧
修飾子 説明
+readonly readonlyを追加する。(+は省略可能)
-readonly readonlyを削除する。
+? オプショナルを追加する。(+は省略可能)
-? オプショナルを削除する。(Requiredに相当)


  • -readonly を使用して全ての readonly を取り除く Mutable 型の例
     type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
     
     interface Frozen { readonly x: number; readonly y: number; }
     type MutablePoint = Mutable<Frozen>;
     // { x: number; y: number; }
    

  • -? を使用して全てのオプショナルを取り除く Required 型の例
     type MyRequired<T> = { [K in keyof T]-?: T[K]; };
     
     interface Options { host?: string; port?: number; }
     type RequiredOptions = MyRequired<Options>;
     // { host: string; port: number; }
    


キーのリマッピング

TypeScript 4.1以降では、マップ型に as 句を追加してキーをリマッピングできる。
テンプレートリテラル型と組み合わせることで、プロパティ名を動的に変換した新しい型を作成できる。

  • 各プロパティに対応するゲッターメソッドを持つ型を生成する例
     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; }
    

  • イベントハンドラの型を生成する例
     type EventHandlers<T extends Record<string, any>> = {
        [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
     };
     
     interface FormEvents { change: string; submit: boolean; }
     type FormEventHandlers = EventHandlers<FormEvents>;
     // { onChange: (value: string) => void; onSubmit: (value: boolean) => void; }
    

  • as 句で never を返すことで、条件に一致しないキーを除外するフィルタリングも実現できる。
     // 特定の型を持つプロパティのみを抽出する
     type PropsByType<T, U> = {
        [K in keyof T as T[K] extends U ? K : never]: T[K];
     };
     
     interface Mixed { name: string; age: number; active: boolean; score: number; }
     type NumberProps = PropsByType<Mixed, number>;
     // { age: number; score: number; }
    

  • オブジェクトの関数プロパティの名前のみを取得する応用例
     type FunctionNames<T> = {
        [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
     }[keyof T];
     
     interface Service {
        name: string;
        connect: () => void;
        disconnect: () => void;
        retry: (times: number) => boolean;
     }
     
     type ServiceMethods = FunctionNames<Service>;
     // "connect" | "disconnect" | "retry"
    



関連情報