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を使うことができます。任意の文字を表す場合は、T
やU
を使うことが一般的です。
// 第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
に代入できる型は、string
、number
、symbol
とそれぞれのリテラル型です。
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型を定義すると、予期しない型まで許容してしまうことになります。
以上です。