吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

【TypeScript】ジェネリック型について

2014年10月20日 / category : typescript

TypeScriptの大きな機能のひとつに、ジェネリックという仕組みがあります。
この仕組みはいろんな使い方があり、正直なところ、僕自身まだ完全に把握できていません。
理解しているところを中心に解説してみます。

まず、ジェネリックとはどういう仕組みか。
簡単に言うと、関数やクラス、インターフェースの型定義をパラメータ化できるという仕組みです。
例えば、次のコードを見てみます。

TypeScript
function A(value:string):string{
	return value;
};

function B(value:number):number{
	return value;
};

console.log( A('Hello'), B(100) );

‘Hello 100′
が出力されます。

A関数、B関数の処理内容を見ると

return value;

と、同じ処理を行っています。この2つの関数の違いはというと、引数、返り値で指定している型指定が異なります。
この型指定をパラメータ化することを、ジェネリック(ジェネリック型)と言います。

ジェネリック型 – 関数

次のコードは、ジェネリック型として定義した関数です。

TypeScript
function C<T>(value:T):T{
	return value;
};

console.log( C<string>('Hello'), C<number>(100) );

‘Hello 100′
先ほどと同じ値が出力されます。

関数をジェネリック型で定義すると次のような構文になります。

■関数定義
function 関数名<型パラメータ>(引数:型パラメータ):型パラメータ{}

■実行文
関数名<型パラメータ>(値)

ジェネリック型が複数必要な場合は、次のような記述になります。

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

D<string, number>('Hello', 100);

‘Hello’
’100′
が出力されます。

ジェネリック型が複数必要な場合は、コンマ(,)区切りで型を繋げて記述します。
型パラメータを使用する場合は、大文字1文字を使用するのが通常らしいです。

ジェネリック型 – アロー関数

ジェネリック型の定義をアロー関数で使用すると、
次のような記述になります。

TypeScript
var A = <T>(value:T):T => value;
console.log( A<string>('Hello'), A<number>(100) );

var B = <T,U>(t:T, u:U) => {
	console.log(t);
	console.log(u);
};
B<string, number>('Hello', 100);
B<number, string>(100, 'Hello');

‘Hello 100′
‘Hello’
’100′
’100′
‘Hello’
が出力されます。

これは、想像できたカタチかと思いますので、解説は割愛します。

ジェネリック型 – クラス

このジェネリック型の定義はクラスでも使用することができます。
早速、次のコードを見てみます。

TypeScript
class A<T>{
	public value:T;
	constructor(t:T){
		this.value = t;
	}
	public getValue():T{
		return this.value;
	}
}

var a0:A<string> = new A<string>('Hello');
console.log( a0.getValue() );

var a1:A<number> = new A<number>(100);
console.log( a1.getValue() );

‘Hello’
’100′
が出力されました。

クラス名 + <型パラメータ>

ジェネリック型でクラスを定義する場合は、上のような構文になります。

想像できると思いますが、複数の場合は、次のようなコードになります。

TypeScript
class A<T, U>{
	public t:T;
	public u:U;
	constructor(t:T, u:U){
		this.t = t;
		this.u = u;
	}
	public value(){
		console.log(this.t);
		console.log(this.u);
	}
}

var a0:A<string, number> = new A<string, number>('Hello', 100);
a0.value();

var a1:A<number, string> = new A<number, string>(100, 'Hello');
a1.value();

ジェネリック型 – インターフェイス

インターフェイスに、ジェネリック型を定義するときは、次のようなコードになります。

TypeScript
interface A<T,U>{
	t:T;
	u:U;
	value();
}

class ClassA<T,U> implements A<T,U>{
	public t:T;
	public u:U;
	constructor(t:T, u:U){
		this.t = t;
		this.u = u;
	}
	public value(){
		console.log( this.t );
		console.log( this.u );
	}
}

var a0:ClassA<string, number> = new ClassA<string, number>('Hello', 100);
a0.value();

var a1:ClassA<number, string> = new ClassA<number, string>(100, 'Hello');
a1.value();

ジェネリック型 – 配列

配列の型指定をするときにも、ジェネリック型を使用できます。

TypeScript
function getFirstValue<T>(arr:T[]):T{
	return arr[0];
};

console.log( getFirstValue<string>(['a','b','c']) );
console.log( getFirstValue<number>([100,200,300]) );

ジェネリック型 – 継承(制約)

ジェネリック型の定義に対して、extends(継承)の仕組みを設定することができます。
記述は次のようになります。

<T extends A>

これで、「Aを継承したT」となります。
結果どうなるか。次のコードを見てみます。

TypeScript
function A<T extends string>(value:T){
	return value;
};

console.log( A<string>('Hello') );
console.log( A<number>(100) ); //コンパイルエラー

2目のコンソールがコンパイルエラーになります。
これはどういうことかというと、

<T extends string>

とすることで、ジェネリック型のTは、string型を継承することになり、string型に制約されることになります。
そのため、<string>以外の型指定をすると、コンパイルエラーになります。

でも・・・、これってあまり意味がないですよね・・・、ジェネリック型にしているのに、型で制約してしまうと・・・。
まだ勉強中ですが、恐らく・・・こういう使い方はしないのかな・・・と、思っています。

では、どのような使い方をするか。

<T extends A>

このAの部分。
ここには、型以外にも指定することができます。

TypeScript
class Sample{
	public name:string;
	public age:number;
	constructor(_name:string, _age:number){
		this.name = _name;
		this.age = _age;
	}
}

function A<T extends Sample>(value:T){
	return value.name + ' : ' + value.age;
};

var a:Sample = new Sample('tsudoi', 33);
console.log( A<Sample>(a) );

‘tsudoi : 33′
が出力されます。

上のコードでは、クラスを継承させています。
少々複雑ですが、ジェネリック型にクラスを継承させることで、引数valueにクラスが持っているプロパティやメソッドを使えるようになります。

TypeScript
interface Sample{
	name:string;
	age:number;
}

class ClassA implements Sample{
	public name:string;
	public age:number;
	constructor(_name:string, _age:number){
		this.name = _name;
		this.age = _age;
	}
}

class ClassB implements Sample{
	public name:string;
	public age:number;
	constructor(_age:number, _name:string){
		this.name = _name;
		this.age = _age;
	}
}

function A<T extends Sample>(value:T){
	return value.name + ' : ' + value.age;
};

var a:ClassA = new ClassA('tsudoi', 33);
var b:ClassB = new ClassB(33, 'tsudoi');

console.log( A<Sample>(a) );
console.log( A<Sample>(b) );

‘tsudoi : 33′
‘tsudoi : 33′
が出力されます。

上のコードでは、interfaceを継承した例です。
少々複雑ですが、問題なさそうです。

TypeScript
function A<T extends {name:string; age:number;}>(value:T){
	return value.name + ' : ' + value.age;
};

var obj = {
	name: 'tsudoi',
	age: 33
};
console.log( A(obj) );

‘tsudoi : 33′
が出力されます。

クラスやinterface以外に、「オブジェクト型リテラル」という指定で継承させることができますが、「オブジェクト型リテラル」については別途解説したいと思います。
ここでは、割愛します。

と、ジェネリック型の継承について、できることを書いてきましたが、
それでもまだ、いまいち使い方がよくわかりませんよね・・・。

上の例文では、全てプロパティを使用しています。
それでは、次のコードを見てみましょう。

TypeScript
class A{
	public test(){
		return 'test';
	}
}

function myFunc<T extends A>(value:T){
	return value.test();
};

var a:A = new A();
console.log( myFunc(a) );

‘test’
が出力されます。

これは・・・、何がどうなっているのでしょうか。

ジェネリック型のTが、クラスAを継承しています。
クラスAには、testメソッドが設定されていて、’test’の文字列を返しています。

変数aには、クラスAのインスタンスを代入しています。
myFunc関数には、その変数aを引数として設定しています。

myFunc関数の実行処理を見てみます。

return value.test();

引数valueには、変数aすなわちクラスAのインスタンスが代入されています。
このインスタンスはtestメソッドを持っていますので、実行することができます。
testメソッドは、’test’の文字列を返していますので、ここではさらにreturnされ、結果、コンソールに’test’が出力されることになります。

ここで、何が言いたいかというと、ジェネリック型が継承したクラスのプロパティだけでなく、メソッドも使用することができる。
ということ。
これで少しジェネリック型の良い使い方が見えてくるかも?

という、例を次に書いてみます。

TypeScript
interface Base{
	test();
}

class A implements Base{
	private numA;
	private numB;
	constructor(_numA:number, _numB:number){
		this.numA = _numA;
		this.numB = _numB;
	}
	public test(){
		return this.numA + this.numB;
	}
}

class B implements Base{
	private numA;
	private numB;
	constructor(_numA:number, _numB:number){
		this.numA = _numA;
		this.numB = _numB;
	}
	public test(){
		return this.numA - this.numB;
	}
}

function myFunc<T extends Base>(value:T){
	return value.test();
}

var a:A = new A(50, 25);
var b:B = new B(50, 25);

console.log( myFunc(a) );
console.log( myFunc(b) );

’75′
’25′
が出力されます。

コードが長くなってしまいましたが、やっていることは簡単です。
まず、クラスAとクラスBの違いは、コンストラクタ関数の引数の順番が違うという点と、testメソッドの処理内容が違います。
クラスAのtestメソッドは2つのプロパティを足し算、クラスBは引き算しています。

myFunc関数のジェネリック型Tには、interfaceのBaseを継承しています。
このBaseは、クラスA、クラスBともにimplementsしています。

変数aと変数bには、それぞれクラスA、クラスBのインスタンスを代入し、引数には同じ値(50,25)を設定しています。
この変数をそれぞれmyFunc関数の引数に代入しています。

結果、、、どうなるか。

myFunc関数の引数valueには、それぞれのクラスのインスタンスが格納されていますが、testメソッドは持っています。
ただし、testメソッドの処理は異なるため、返り値も異なります。

ということで、

’75′
’25′

と、異なる値が出力されるわけです。

と、、、、ま、、、こんな使い方ができるんじゃないかな~、と言った感じですが、
まだ実務では使っていません。。。