🧬

TypeScript Generics 公式ドキュメント

に公開

はじめに

TypeScript の Generics(ジェネリクス)は、型安全性を保ちながら再利用可能なコードを書くための機能です。

TypeScript 公式ドキュメントの内容に基づき、Generics の基本概念から実践的な使用パターンまで、コード例とともに整理しています。

Hello World of Generics

identity 関数の例

Generics を使用しない場合、特定の型を指定する必要があります:

function identity(arg: number): number {
  return arg;
}

または、any 型を使用した場合:

function identity(arg: any): any {
  return arg;
}

// 型安全性が失われる
const result = identity("hello"); // any型
// result.toFixed(); // 実行時エラー!コンパイル時に検出できない

any を使用すると、引数の型に関する情報が失われ、戻り値の型も不明になります。

型変数の使用

Generics では型変数を使用してこの問題を解決します:

function identity<Type>(arg: Type): Type {
  return arg;
}

Type は型変数(type variable)で、ユーザーが提供する型(例:number)を捕捉し、その情報を後で使用できます。

Generic 関数の呼び出し方法

Generic 関数は 2 つの方法で呼び出せます:

// 1. 明示的に型引数を指定
let output = identity<string>("myString");

// 2. 型引数推論を使用(一般的)
let output = identity("myString"); // stringと自動推論

型引数推論では、コンパイラが引数の値を見て自動的に型を設定します。

Working with Generic Type Variables

Generic 型変数の制限

Generic 関数を作成する際、コンパイラは型パラメータを正しく使用することを強制します。型パラメータは任意の型を表すため、その型が持つプロパティにアクセスできません。

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // ❌ Error: Property 'length' does not exist on type 'Type'
  return arg;
}

Typelength プロパティを持つ保証がないため、エラーになります。

配列を使用した解決方法

Type の配列として扱う場合、.length プロパティが使用できます:

function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length); // ✅ OK: 配列には length プロパティがある
  return arg;
}

または、Array<Type> 形式で記述することもできます:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // ✅ OK: Array には length プロパティがある
  return arg;
}

これにより、Generic 型変数 Type を全体の型ではなく、操作する型の一部として使用でき、より柔軟性が得られます。

Generic Types

Generic 関数の型

Generic 関数の型は、通常の関数と同様に、型パラメータが最初にリストされます:

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: <Type>(arg: Type) => Type = identity;

型パラメータの命名

型変数には異なる名前を使用できます(数と使用方法が一致していれば):

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: <Input>(arg: Input) => Input = identity;

オブジェクトリテラル型

Generic 型をオブジェクトリテラルの呼び出しシグネチャとして記述できます:

let myIdentity: { <Type>(arg: Type): Type } = identity;

Generic インターフェース

上記のオブジェクトリテラルをインターフェースに移行できます:

interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: GenericIdentityFn = identity;

インターフェース全体の Generic パラメータ

型パラメータをインターフェース全体のパラメータとして移動できます:

interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

この場合、GenericIdentityFn を使用する際に対応する型引数(number)を指定する必要があります。

Generic Classes

基本的な Generic クラス

Generic クラスは、Generic インターフェースと同様の形状を持ちます。クラス名の後に角括弧(<>)内に Generic 型パラメータリストを持ちます:

class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

異なる型での使用

GenericNumber クラスは number 型に限定されません。string やより複雑なオブジェクトでも使用できます:

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

Generic クラスの制限

Generic クラスは、インスタンス側でのみ Generic です。静的側では Generic ではありません。クラスの静的メンバーは、クラスの型パラメータを使用できません。

Generic Constraints

基本的な制約

特定の機能を持つ型のセットで動作する Generic 関数を作成したい場合があります。loggingIdentity の例では、.length プロパティにアクセスしたいが、コンパイラはすべての型が .length プロパティを持つことを証明できません:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // ❌ Error: Property 'length' does not exist on type 'Type'
  return arg;
}

extends キーワードによる制約

制約を表すインターフェースを作成し、extends キーワードを使用して制約を示します:

interface Lengthwise {
  length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // ✅ OK: .length プロパティが保証される
  return arg;
}

Generic 関数が制約されているため、すべての型で動作しなくなります:

loggingIdentity(3); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'

代わりに、必要なプロパティを持つ値を渡す必要があります:

loggingIdentity({ length: 10, value: 3 }); // ✅ OK

Using Type Parameters in Generic Constraints

型パラメータ間の制約

ある型パラメータを別の型パラメータによって制約できます。オブジェクトから存在しないプロパティを取得しないようにする例:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // ✅ OK
getProperty(x, "m"); // ❌ Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'

Using Class Types in Generics

コンストラクタ関数による型指定

TypeScript で Generics を使用してファクトリを作成する場合、コンストラクタ関数でクラス型を参照する必要があります:

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

より高度な例

プロトタイプ プロパティを使用して、コンストラクタ関数とクラス型のインスタンス側の関係を推論し制約する例:

class BeeKeeper {
  hasMask: boolean = true;
}

class ZooKeeper {
  nametag: string = "Mikle";
}

class Animal {
  numLegs: number = 4;
}

class Bee extends Animal {
  numLegs = 6;
  keeper: BeeKeeper = new BeeKeeper();
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

Generic Parameter Defaults

デフォルト型パラメータの宣言

Generic 型パラメータにデフォルトを宣言することで、対応する型引数の指定を任意にできます:

declare function create<
  T extends HTMLElement = HTMLDivElement,
  U extends HTMLElement[] = T[]
>(element?: T, children?: U): Container<T, U>;

const div = create();
//    ^? const div: Container<HTMLDivElement, HTMLDivElement[]>

const p = create(new HTMLParagraphElement());
//    ^? const p: Container<HTMLParagraphElement, HTMLParagraphElement[]>

Generic パラメータのデフォルトルール

  • デフォルトを持つ型パラメータは任意とみなされます
  • 必須の型パラメータは任意の型パラメータの後に続いてはいけません
  • 型パラメータのデフォルト型は、制約が存在する場合、その制約を満たす必要があります
  • 型引数を指定する際、必須の型パラメータの型引数のみを指定する必要があります
  • デフォルト型が指定され、推論が候補を選択できない場合、デフォルト型が推論されます
  • 既存のクラスまたはインターフェース宣言とマージするクラスまたはインターフェース宣言は、既存の型パラメータにデフォルトを導入できます
  • 既存のクラスまたはインターフェース宣言とマージするクラスまたはインターフェース宣言は、デフォルトを指定する限り、新しい型パラメータを導入できます

Variance Annotations

基本概念

これは非常に特定の問題を解決するための上級機能であり、使用する理由を特定した状況でのみ使用すべきです。

共変性(covariance)と反変性(contravariance)は、2 つの Generic 型の関係を記述する型理論の用語です。

Producer と Consumer の例

オブジェクトが特定の型を make(生産)できることを表すインターフェース:

interface Producer<T> {
  make(): T;
}

Producer<Animal> が期待される場所で Producer<Cat> を使用できます。CatAnimal だからです。この関係を共変性(covariance)と呼びます。

逆に、特定の型を consume(消費)できるインターフェース:

interface Consumer<T> {
  consume: (arg: T) => void;
}

Consumer<Cat> が期待される場所で Consumer<Animal> を使用できます。Animal を受け入れることができる関数は、Cat も受け入れることができるからです。この関係を反変性(contravariance)と呼びます。

分散注釈の記述

非常に稀なケースで、循環型を含む特定の種類の型において、分散の測定が不正確になる場合があります。その場合、型パラメータに分散注釈を追加して特定の分散を強制できます:

// 反変性注釈
interface Consumer<in T> {
  consume: (arg: T) => void;
}

// 共変性注釈
interface Producer<out T> {
  make(): T;
}

// 不変性注釈
interface ProducerConsumer<in out T> {
  consume: (arg: T) => void;
  make(): T;
}

重要な注意事項

分散注釈は、構造的な分散と一致する場合にのみ記述してください。

分散注釈は型デバッグの状況で一時的に有用な場合があります。TypeScript は、注釈付きの分散が明らかに間違っている場合にエラーを発行します:

// Error: このインターフェースは確実に T に対して反変性
interface Foo<out T> {
  consume: (arg: T) => void;
}

まとめ

TypeScript の Generics は、型安全性と再利用性を両立させる機能です。

参考リンク

https://www.typescriptlang.org/docs/handbook/2/generics.html

Discussion