【Typescript】 ジェネリクスと、EnumのようなUnionを理解する

今までは、クラスの中で扱う変数は、クラスを定義する人が書いてきました。ジェネリクスを用いることで型の定義を後回しにし、使う瞬間にどんな型を使うかを指定することができます。

ジェネリクスを使う

多くの場合は、ジェネリクスに対応したクラスを作るよりも、使うことのほうが多いと思います。まずは使い方を見ていきましょう。

ジェネリクスを使ってクラスのインスタンスを作る際は、new クラス名<型>(...)のように書きます。

function f(): Promise<boolean> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(true);
        }, 1000);
    })
}

Javaでは同様の機能が同名で存在します。C++の場合、テンプレートという機能を使用して同様のこと (+拡張したもの) を利用できます。

C++のテンプレートは、それはそれは勉強のしがいがある内容です…

ジェネリクス対応のクラスや関数を作る

ジェネリクスに対応させるには、<T>といったものを書きます。

このTは、実際に使うときに型を指定すると、その型に置き換わるものです。

// 関数の場合
function f<T>(hoge: T): T {
    console.log(hoge);
    return hoge;
}

// 複数の型を指定する場合はカンマで区切る
function f<T, S>(hoge: T, fuga: S): T {
    console.log(hoge);
    return hoge;
}

// classの場合
class MyArray<T> {
    private ary: T[] = [];

    public push(elem: T): number {
        return this.ary.push(elem);
    }

    public at(pos: number): T | undefined {
        if (pos >= this.ary.length) {
            return undefined;
        }
        if (pos < 0) {
            return this.at(pos + this.ary.length);
        }
        return this.ary[pos];
    }

    // メソッドにもジェネリクスを使える
    public f<S>(hoge: S) {
        // ...
    }
}

このように書くことで、const ary = new MyArray<number>();のように、普段のジェネリクスの型指定を使って書くことができます。

ジェネリクスでは、指定した型はインスタンスごとに管理されます。

const ary = new MyArray<number>();
ary.push(100);

const ary2 = new MyArray<string>();
ary2.push("hoge");

特定の型を継承したものに制限する

ジェネリクスを使う場合、特定の型を継承したものに制限できます。

type Addable = number | string;

function appendText<T extends Addable, S extends Addable>(lhs: T, rhs: S): string {
    return lhs.toString() + rhs.toString();
}

appendText(100, "hoge"); // 100hoge

この例でもしextendsしなかった場合、足し算が定義されていない方が入ってくる可能性があるため、エラーとなります。

特定の機能を持つものに制限することで、特定の型であること前提のコートを書くことができます。

enumのようなunion

また、特定の値のみを受け付ける、といったこともあります。

function setColor<T extends "red" | "blue">(color: T) {
    // ...
}
setColor("red");
setColor("yellow"); // NG

このような指定はenumと似ていますが、いくつかの理由からenumよりunionが推奨されています。

Tree-Shakingがうまく行かない、enumの値が数値の場合に数値を代入できてしまうなどあります。

enumのような使い勝手でunionをするには、次のように書きます。

// as constを付けることで、代入だけでなく中身の書き換えもできなくなる
const Color = {
    Red: "red",
    Blue: "blue",
    Yellow: "yellow",
} as const;

// type Color = "red" | "blue" | "yellow" になる
type Color = typeof Color[keyof typeof Color];

function setColor<T extends Color>(color: Color) {
    // ...
}

setColor("red"); // OK
setColor(Color.Red); // OK
setColor("purple"); // NG

keyofというものが初めて出てきましたが、この場面でしか使用しません。

keyofは、オブジェクトのプロパティ一覧を直和型にするものです。上の例だと"Red", "Blue", "Yellow"になります。それをtypeof Color[...]で囲むことで、そのキーに対応する値の直和型にしています。

enumでは問題点として上がっていたものが、unionでは改善されているため、基本的にはこちらを使うことを推奨します。

もし何らかの理由で実際の値を隠蔽したい場合は、enumを使うと良いと思います。

ジェネリクスを使うと何が良いのか

何でも受け付けるanyと比べてどう良いのでしょうか。

  • 型を使った入力補完が効く
  • 異なる型が入ると困る場合に制限できる (エラーにできる)
  • どんな値が入っているのかが型でわかる

いずれも、入力を制限することでスムーズに開発を進めるためにもの、ということになります。

anyを使えば何でも受け付けられますが、それではJavascriptと変わりません。ジェネリクスをうまく使いこなして、堅実なプログラムを書いていく必要があります。

まとめ

ジェネリクスは多くの静的型付け言語でしばしば利用できる機能です。Typescriptではその基本機能に加え、unionを用いてenumより賢く型を定義できます。

ジェネリクスでは、型を制限することで内部をうまく実装できます。extendsを使用した制限は必ず覚えておきましょう!

ts generics thumb

役に立ったらシェアしよう!