Typescript(ジェネリクス)

はじめに

今回は他の言語でも利用頻度が高いジェネリクス(ジェネリック)についてです。以前の記事でジェネリクスを使った例は配列(Array型)にありました。

let chara1: string[] = ["naruto", "sasuke", "sakura"];
let chara2: Array<string> = ["naruto", "sasuke", "sakura"];

ここで注目したいのが、型指定でstringとArrayは全くもって同じ(どちらも文字列の配列)ということです。chara2にマウスホバーしてみるとstringと推論されます。stringを消すとエラーが発生します。
ジェネリック型 'Array' には 1 個の型引数が必要です。ts(2314)


ジェネリック型とは、他の特定の型と結合された型です。例えば、

const arry: Array<> = [];エラー(ジェネリック型 'Array<T>' には 1 個の型引数が必要です。)

このように空配列の場合、山括弧<>の中(型引数)によって様々な配列を作り出すことができます。

const arry: Array<number> = [1, 2, 3];
const arry1: Array<boolean> = [true, false];
const arry2: Array<number | boolean> = [1, 2, true, false];

ジェネリックの使い方

これは大事なことですが、ジェネリクスは抽象的な型引数を使用して、実際に利用されるまで型が確定しないクラス・関数・インターフェイスを実現する為に使用されます。以下、前回までの記事のコードをジェネリクスで書き換えていきます。

①関数
//ジェネリクスなし
function merge(arry1: number[], arry2: string[]) {
  return [...arry1, ...arry2];
}
console.log(merge([1, 2], ["Mikasa", "Reiner"]));
//(4) [1, 2, "Mikasa", "Reiner"]



//ジェネリクスあり
function merge<T, U>(arry1: T[], arry2: U[]) {
  return [...arry1, ...arry2];
}
console.log(merge<number, string>([1, 2], ["Mikasa", "Reiner"]));
//(4) [1, 2, "Mikasa", "Reiner"]

ジェネリクスありだと関数を定義した時点では、TやUにどの型が入るかまだわからないので型の定義に柔軟性が付きます。因みにTを使うのは慣習的(type(型)の頭文字からT)にTからアルファベット順で定義するそうです。

②クラス
//ジェネリクスなし
class Person {
  constructor(private readonly name: string, public age: number) {}
  hello() {
    this.name = "Green"; //エラー(読み取り専用プロパティであるため、'name' に代入することはできません。)
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト



//ジェネリクスあり
class Person<T extends string, U extends number> {
  constructor(private readonly name: T, public age: U) {}
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person<string, number>("Bob", 22); //生成されるインスタンスはオブジェクト
bob.hello();//hello Bob

この例はジェネリクスの理解をするためにはいいかもしれませんが、用途としては良くないでしょう。なぜなら、プロパティ名はname,numberでありそれぞれの型は型がstring型,number型だと一目瞭然のため、クラスの定義の段階で(ジェネリクスによって型定義を焦らすのではなく)型を定義するべきだからです。あくまでジェネリクスの理解のためだとご了承ください。
ここで新たにクラス継承ではおなじみのextendsキーワードが登場しています。T extends stringとはTはstring型の部分的な型、つまりはTがstring型であることを表現しています。純粋なジェネリクスであればクラス定義の段階ではTやUにはどのような型が入るかわかりませんが、extendsキーワードによってジェネリクスに制限を加えることでクラス定義の段階でTやUの型を絞り込むことが可能です。extendsキーワードによって、型の安全性を保証してくれます。

③インターフェース

最後のインターフェース編ではやや応用例を扱います。

function numerableArray<T>(element: T) {
  let descriptionText = "値がありません。";
  if (element.length > 0) {//エラー(プロパティ 'length' は型 'T' に存在しません。)
    descriptionText = `値は${element.length}です。`;
  }
  return [element, descriptionText];
}

このジェネリクスによるnumerableArray関数は現状エラーが発生しています。なぜなら、Tを型に持つelementがlengthプロパティを持つかどうかわからないためです。このエラーをインターフェース、extendsキーワードを使って修正します。

interface Lengthy {
  length: number;
}
function numerableArray<T extends Lengthy>(element: T) {
  let descriptionText = "値がありません。";
  if (element.length > 0) {
    descriptionText = `値は${element.length}です。`;
  }
  return [element, descriptionText];
}
console.log(numerableArray("Eren Jaeger"));
//(2) ["Eren Jaeger", "値は11です。"]

LengthyインターフェースをTに継承することによって、T型(のelement)がlengthプロパティを持つことを保証します。これによりエラーなく実行します。またlengthプロパティをもつのはstring型とArray型のみです。これによりelementにnumber型を代入しようとするとエラーが発生します。

interface Lengthy {
  length: number;
}
function numerableArray<T extends Lengthy>(element: T) {
  let descriptionText = "値がありません。";
  if (element.length > 0) {
    descriptionText = `値は${element.length}です。`;
  }
  return [element, descriptionText];
}
console.log(numerableArray(123));
//エラー(型 '123' の引数を型 'Lengthy' のパラメーターに割り当てることはできません。)