Typescript|ジェネリクスの意味と使い方について

開発

ジェネリクスとは

型を抽象化するためのもの。
型引数を使用して、実際に利用されるまで型が確定しないクラスや関数を実現するためのもの。

関数の場合

具体的には下記のがジェネリクスにあたる。
下記では、関数aの引数の型は、実行時まで確定しない。

function a<T>(x: T) {
  console.log(x);
}

a<number>(2);

関数の内部で使用するだけでもよい

function b<T>() {
  var x: T = null;
  console.log(x)
}

a<number>():

型引数が複数あってもよい。
以下のようにカンマで区切って表現する。
アルファベット順で使う事が多いようだ。

function c<T,U> (t: T, u: U){
  console.log(t);
  console.log(u);
}

クラスの場合

class MyClass<T>{
  public name: T;

}

例えば、以下のように型引数で型付けされたデータを使用すると、コンパイルエラーになる。

interface X {
    sayHi();
}

class Y implements X {
    public sayHi() {
        console.log("Hi");
    }
}

function getSayHi<T>(t: T){
    t.sayHi();
}

getSayHi(new Y());

コンパイル結果は下記。
Tは未知の型なので、YクラスにsayHiメソッドが存在するかどうかは判断できない。

src/ts.ts:12:4 - error TS2339: Property 'sayHi' does not exist on type 'T'.

このエラーを解決するためには、ジェネリクスに制約を入れる必要がある。

ジェネリクスの制約

型引数に制約を追加するには、まずsayHiメソッドが存在することを型として定義する必要がある。

下記は、「Tの型は、X型もしくはXを継承した型である」という制約を付与している。
extendsという単語を使って、型引数に制約を追加している。

function getSayHi<T extends X }>(t: T){
    t.sayHi();
}

制約を付与できるのは、Typescriptの型システムによるところが大きい。

このブログによると、

TypeScriptは「Structural typing(構造型型付け)」と呼ばれる、構造が同じであれば同じ型とみなす方式を取っているので、このような柔軟な型宣言が可能

とのこと。

つまり、同じメンバーを持っており、構造が同じであれば、互換性のある型とみなされる。

ちなみに、Javaなどの派生型を基本的な型と認識する型システムはNominal typing =公称型型付けと呼ばれる。

メンバー指定で制約を追加する

下記のように、利用メンバーだけを指定して制約を付与することもできる。
仮にXインターフェースに他のメンバーが存在していたとしても、型引数TはsayHi()メンバだけを型として使用できる。

function getSayHi<T extends{ sayHi(); }>(t: T){
    t.sayHi();
}

まとめ

Typescriptの型システムを理解するうえで重要なジェネリクスについて解説してみた。
なにかの参考になれば幸いに思う。

参考

TypeScriptの目玉機能「ジェネリック(Generics)」はこうなっている – Build Insider https://www.buildinsider.net/language/tsgeneric/01

TypeScriptでジェネリクス(Generics)を理解するための簡単なチュートリアル | I am mitsuruog https://blog.mitsuruog.info/2019/03/try-typescript-generics-101

コメント

タイトルとURLをコピーしました