これは何
日常的にTypeScript/Reactでコードを書いているものの、主にUIの作り込みが多いのでTypeScriptの知識としてはそこまで深く知らなくても仕事がこなせてました。
そこで、ちゃんとTypeScriptを書けるようになりたいと思い改めて知らなかったことを書いた記事になります。
ジェネリクスを知ったきっかけ
type-challengesが流行っているので、試しに挑むも1問も解けず、改めてサバイバルTypeScriptから勉強し直したのが、ジェネリクス型を知ったきっかけでもあります。
ジェネリクスとはどういう意味?
ジェネリクス(generics)とは、プログラミング言語の機能・仕様の一つで、同じコードで様々な異なるデータ型のデータを処理できるようにする仕組み。C++言語などでは、ほぼ同様の機能を「テンプレート」(template)という。
IT用語辞典より
プログラミングにおいて、特定のデータ型に縛られることなく、さまざまなデータ型に対応できる柔軟なプログラムを作成することが出来るようになるようです。
実際の例
function identity(arg: any): any {
return arg;
}
上記のコードは公式のコードからコピペしてきたものです。
引数の型がany
になっています。
これだと、あらゆる型を受け入れるという意味では汎用的ではあるけど、そもそもの型情報が何だったのかが分からなくなります。
そしてコンパイル時に型のチェックが行われないため、予期しない型が渡された場合にエラーが発生する可能性があります。
公式のコードを少し変更してみます。
const identity(arg: any): any {
console.log(arg.toUpperCase()); //stringを想定している
}
console.log(identity("hello"));
console.log(identity(1)); //実行されるまでエラーは出ない
すると以下の様な実行結果が出てきます。
HELLO
error: Uncaught (in promise) TypeError: arg.toUpperCase is not a function
この場合、arg
が文字列であることを仮定して.toUpperCase()
を呼び出していますが、数値を渡すとエラーになります。ジェネリクスを使えば、このような問題を防げます。
ジェネリクスを使用したコードが以下です。
function identity<Type>(arg: Type): Type {
return arg;
}
const hello = identity<string>("hello").toUpperCase();
const one = identity<number>(1);
console.log(hello);
console.log(one);
実行結果が以下になります。
HELLO
1
型引数を省略することもできるようです。
const hello = identity("hello").toUpperCase(); // string
const one = identity(1); // number
ジェネリクスの型変数
さきほどのコードで書いた関数identity
の後に記述した<Type>
というのが型変数になります。
ここで名前をType
と付けていますが型「変数」なので、ある程度自由に名前を付けることができます。
ただ慣習みたいなものがあり、大文字のT
で書かれることが多いようです。
function identity<T>(arg: T): T { //型変数をTに変更
return arg;
}
TypeScriptの慣習として、型変数名にはTを用いることが多いです。このTはtemplateの略と言われています。
単純なジェネリクスで、型変数が2つある場合は、TとUが用いられることがあり、その理由はアルファベット順でTの次がUだからです。この規則にしたがって、3つ目の型変数はVとする場合もあります。
型変数の慣習より
型引数に制約をつける
ジェネリクスの型引数を特定の型に限定することができます。
function identity<T extends string | boolean>(arg: T): T {
return arg;
}
型変数T
の後にextends
と記述してジェネリクスの型Tを特定の型に限定することができます。
上記ではstring型とboolean型に限定しています。
では下記のコードはどうなったのでしょうか?
const hello = identity<string>("hello").toUpperCase();
const one = identity<number>(1);
エディタ上でちゃんとエラーが出ていることが分かります。
extends
以外でもimplements
というキーワードがあり、implements
はインターフェースでの限定に使われるようです。
interface ValueObject<T> {
value: T;
toString(): string;
}
class UserID implements ValueObject<number> {
public value: number;
public constructor(value: number) {
this.value = value;
}
public toString(): string {
return `${this.value}`;
}
}
class Entity<ID extends ValueObject<unknown>> {
private id: ID;
public constructor(id: ID) {
this.id = id;
}
//...
}
EntityクラスはValueObjectインターフェースを実装しているクラスをIDとして受ける構造になっていますが19行目にあるようにこのときの型引数の制約はimplementsではなくextendsでなければなりません。
あとextendsは一つしか継承できないけど、implementsは複数継承できるという違いがあるようです。
参考