Generics(Typescriptを学んでいこう #6)

Generics(Typescriptを学んでいこう #6)

今回はGenericsについて学んでいきます。

ジェネリック型(Generics)とは何か

型の安全性を保ちつつ、柔軟性と再利用性を提供する機能です。具体的には、型をパラメータとして関数やクラスに渡すことができます。一度定義するだけで、さまざまな型で動作する関数やクラスを作成できます。

const names: Array<string> = []; // string[]
names[0].split('');

const promise: Promise<number> = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(10);
    }, 2000);
});

// stringではないので、下記のコードはエラーになる
// promise.then(data => {
//     data.split(' ');
// });

ジェネリック関数(Generics Function)

関数の引数にGenericsを使うことができます。任意の文字を表す場合は、TUを使うことが一般的です。

// 第1引数と第2引数に異なるパラメータが入ることを定義
function merge<T, U>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}

const mergedObj = merge({ name: 'Max', hobbies: ['sports'] }, { age: 30 });
console.log(mergedObj.age);

型引数の制約(Constraints)

extendsキーワードを用いることでジェネリクスの型Tを特定の型に限定することができます。

// TとUがobject型であることを定義
function merge<T extends object, U extends object>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}

const mergedObj = merge({ name: 'Max', hobbies: ['sports'] }, { age: 30 });
console.log(mergedObj.age);

インターフェースを型引数に使う

extendsキーワードはインターフェースに対しても使います。インターフェースは実装のときはimplementsキーワードを使いますが型引数に使うときはimplementsを使わず同様にextendsを使います。

interface Lengthy {
    length: number;
}

// implementsではなくextendsを使う
function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
    let descriptionText = 'Got no value.';
    if (element.length === 1) {
        descriptionText = 'Got 1 elements.';
    } else if (element.length > 1) {
        descriptionText = `Got ${element.length} elements.`;
    }
    return [element, descriptionText];
}

console.log(countAndDescribe('Hi, there!'));

keyof型演算子

keyof はオブジェクトの型からプロパティ名を型として返す型演算子です。メリットは、保守性が上がる点です。プロパティが何十個もあるようなオブジェクトに、そのプロパティ名のユニオン型を定義する必要が出てきたとします。プロパティ名をすべて転記するとなると、転記漏れや書き間違いもあるでしょう。そういう場合はkeyofを使うとそもそも書き写す必要がないため、便利な上に安全なコーディングができます。

type Book = {
  title: string;
  price: number;
  rating: number;
};
type BookKey = keyof Book;
// 上は次と同じ意味になる
type BookKey = "title" | "price" | "rating";

型制約に使うkeyof

型制約に使うこともできます。

// UがTのプロパティ名であることを保証する
function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U): string {
    return `Value is ${obj[key]}`;
}

console.log(extractAndConvert({ name: 'Max'}, 'name'));

クラスのジェネリック

Genericsはクラスに対しても使うことができます。下記の例では、各メソッドの引数の型をGenericsを使って定義しています。

class DataStorage<T extends string | number | boolean | object> {
    private data: T[] = [];

    addItem(item: T) {
        this.data.push(item);
    }

    removeItem(item: T) {
        if (this.data.indexOf(item) === -1) {
            return;
        }
        this.data.splice(this.data.indexOf(item), 1);
    }

    getItems() {
        return [...this.data];
    }
}

const textStorage = new DataStorage<string>();
textStorage.addItem('Max');
textStorage.addItem('Manu');
textStorage.removeItem('Max');
console.log(textStorage.getItems());

const objStorage = new DataStorage<object>();
const maxObj = {name: 'Max'};
objStorage.addItem(maxObj);
objStorage.addItem({name: 'Maru'});
objStorage.removeItem(maxObj);
console.log(objStorage.getItems());

ユーティリティ型(Utility Type)

ユーティリティ型(utility type)は、型から別の型を導き出してくれる型です。Typescriptに最初から組み込まれているものです。

Required<T>

Required<T>は、Tのすべてのプロパティからオプショナルであることを意味する?を取り除くユーティリティ型です。

type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};
type RequiredPerson = Required<Person>;

// 下記と同じ意味になる
type RequiredPerson = {
  surname: string;
  middleName: string;
  givenName: string;
};

Readonly<T>

Readonly<T>は、オブジェクトの型Tのプロパティをすべて読み取り専用にするユーティリティ型です。

type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};
type ReadonlyPerson = Readonly<Person>;

// 下記と同じ意味になる
type ReadonlyPerson = {
  readonly surname: string;
  readonly middleName?: string;
  readonly givenName: string;
};

Partial<T>

Partial<T>は、オブジェクトの型Tのすべてのプロパティをオプションプロパティにするユーティリティ型です。

type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};
type PartialPerson = Partial<Person>;

// 下記と同じ意味になる
type PartialPerson = {
  surname?: string;
  middleName?: string;
  givenName?: string;
};

Record<Keys, Type>

Record<Keys, Type>はプロパティのキーがKeysであり、プロパティの値がTypeであるオブジェクトの型を作るユーティリティ型です。

Record<Keys, Type>の型引数

Keys

オブジェクトのプロパティーキーを指定します。Keysに代入できる型は、stringnumbersymbolとそれぞれのリテラル型です。

Type

オブジェクトのプロパティの値の型を指定します。任意の型が代入できます。

// キーがstringで値がnumberのインデックス型を定義
type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };

// キーがfirstName、middleName、familyNameで、値が文字列になるオブジェクトの型を定義
type Person = Record<"firstName" | "middleName" | "lastName", string>;
const person: Person = {
  firstName: "Robert",
  middleName: "Cecil",
  lastName: "Martin",
};

Pick<T, Keys>

Pick<T, Keys>は、型TからKeysに指定したキーだけを含むオブジェクトの型を返すユーティリティ型です。

Pick<T, Keys>の型引数

T

型引数Tにはオブジェクトの型を代入します。

Keys

Keysにはオブジェクトの型Tのプロパティキーを指定します。object型Tに存在しないプロパティーキーを指定するとコンパイルエラーになります。

type User = {
  surname: string;
  middleName?: string;
  givenName: string;
  age: number;
  address?: string;
  nationality: string;
  createdAt: string;
  updatedAt: string;
};
type Person = Pick<User, "surname" | "middleName" | "givenName">;

// 下記と同じ意味になる
type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};

Omit<T, Keys>

Omit<T, Keys>は、オブジェクトの型TからKeysで指定したプロパティを除いたobject型を返すユーティリティ型です。

Omit<T, Keys>の型引数

T

型引数Tにはオブジェクトの型を渡します。

Keys

Keysには引数Tのプロパティキーを指定します。ここで指定したプロパティキーと一致するプロパティをTから除去します。

type User = {
  surname: string;
  middleName?: string;
  givenName: string;
  age: number;
  address?: string;
  nationality: string;
  createdAt: string;
  updatedAt: string;
};
type Optional = "age" | "address" | "nationality" | "createdAt" | "updatedAt";
type Person = Omit<User, Optional>;

// 下記と同じ意味になる
type Person = {
  surname: string;
  middleName?: string;
  givenName: string;
};

Exclude<T, U>

Exclude<T, U>は、ユニオン型TからUで指定した型を取り除いたユニオン型を返すユーティリティ型です。

Exclude<T, U>の型引数

T

型引数Tには、ユニオン型を渡します。

U

型引数Uには、Tから取り除きたい型を渡します。

type Grade = "A" | "B" | "C" | "D" | "E";
type PassGrade = Exclude<Grade, "E">;

// 下記と同じ意味になる
type PassGrade = "A" | "B" | "C" | "D";
// 複数指定
type Grade = "A" | "B" | "C" | "D" | "E";
type PassGrade = Exclude<Grade, "D" | "E">;

// 下記と同じ意味になる
type PassGrade = "A" | "B" | "C" | ;

Extract<T, U>

Extract<T, U>は、ユニオン型TからUで指定した型だけを抽出した型を返すユーティリティ型です。

Extract<T, U>の型引数

T

型引数Tには、抽出されるほうのユニオン型を渡します。

U

型引数Uには、抽出したい型を渡します。

type Grade = "A" | "B" | "C" | "D" | "E";
type FailGrade = Extract<Grade, "D" | "E">;

// 下記と同じ意味になる
type FailGrade = "D" | "E"

2つのユニオン型の共通部分を導き出すことにも使えます。

type CommonTypes = Extract<"a" | "b" | "c", "b" | "c" | "d">;

// 下記と同じ意味になる
type CommonTypes = "b" | "c"

ジェネリック型とUnion型の違い

ジェネリック型は型を制約することが目的です。Union型は複数の型のいずれでも使えるよ、ということを示したものです。そのため、本来ジェネリック型を定義しないといけない箇所で、Union型を定義すると、予期しない型まで許容してしまうことになります。

以上です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です