サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

TypeScript: ジェネリック型


TypeScript公式Handbook「Generics」をもとにした解説です。「ジェネリック型(Generics)」は、型づけに変数を用いる仕組みです。型を定めながらも、具体的な型づけはそのときどきで変えられます。その構文と使い方についてご説明します。

01 ジェネリック型を使う

つぎのような簡単な関数を例にとりましょう。数値(number)の引数を受け取って、そのまま返します。数値以外を渡せば、エラーが起こります。


function identity(arg: number): number {
	return arg;
}

同じ関数で文字列も扱いたいとき、やり方のひとつはつぎのようにanyで型づけすることです。けれど、どのようなデータにも置き替えできてしまいます。


function identity(arg: any): any {
	return arg;
}
let output = identity('fumio');
output = [0, 1, 2];  // エラーにならない

ジェネリック型は、以下のように山かっこ<>の中に型変数を入れて定めます。すると、関数を呼び出すときに渡した型が用いられるのです。異なるデータを扱おうとすれば、つぎのようなエラーになります。

Type '1' is not assignable to type 'string'.

function identity<T>(arg: T): T {
	return arg;
}
let output = identity<string>('fumio');
output = 1;  // エラー

ジェネリックで型づけした関数は、呼び出すときに型を渡さなくても、推論が働いて型は定まります。けれども、エラーが起きたときにわかりやすいよう、型を渡しておく方がよいでしょう。


let output = identity('fumio');

ご注意いただきたいのは、型変数にはどのようなデータ型も入るということです。クラスによって異なるプロパティやメソッドを参照すれば、エラーになります。

Property 'length' does not exist on type 'T'.

function getLength<T>(arg: T): number {
	return arg.length;  // プロパティが(あるとはかぎら)ないのでエラー
}

配列要素をつぎのようにジェネリック型で定めた場合であれば、Arrayクラスのプロパティやメソッドは参照できます。


function logElements<T>(arg: T[]): void {
	let length: number = arg.length;
	for (let i: number = 0; i < length; i++) {
		console.log(arg.shift());
	}
}
logElements([0, 1, 2]);

02 ジェネリック型を用いた関数で型づけする

ジェネリック型を使った関数で型づけすることもできます。オブジェクト型リテラルでは、ジェネリック型を用いた関数は、つぎのように定められます。


function identity<T>(arg: T): T {
	return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;

型づけとしてもっと広く使いたい場合には、つぎのようにインタフェースで定義することもできます[*1]


interface IIdentity {
	<T>(arg: T): T;
}
function identity<T>(arg: T): T {
	return arg;
}
let myIdentity: IIdentity = identity;

さらに、インタフェースに定める型そのものを、つぎのようにジェネリック型で与えることができます。この場合、変数(myIdentity)の型づけに用いられたインタフェースは、型が(numberに)決まるということです。そのため、代入した関数にジェネリック型が用いられていても、定められた型しか使えなくなります。


interface IIdentity<T> {
	(arg: T): T;
}
function identity<T>(arg: T): T {
	return arg;
}
let myIdentity: IIdentity<number> = identity;

[*1] 筆者の環境では、戻り値についてはインタフェースか関数のどちらかがジェネリック型である場合には、つぎのように厳密な一致が求められないようです。

interface IIdentity {
	// <T>(arg: T): T;
	<T>(arg: T): string;
}
function identity<T>(arg: T): T {
// function identity<T>(arg: T): string {
	return arg;
	// return 'arg';
}
let myIdentity: IIdentity = identity;
console.log(myIdentity<number>(0));  // エラーにならない

03 ジェネリック型でクラスを定める

ジェネリック型をつぎのようにクラス(SumItems)の定めに用いることもできます。クラスはインタフェースのように扱っていることにご注目ください。クラスの定めの中でジェネリック型のデータのままでは、参照できるプロパティやメソッド、使える演算子がほとんどないからです。


class SumItems<T> {
	zeroValue: T;
	sum: (arg: T[]) => T;
}
let sumNumbers = new SumItems<number>();
sumNumbers.zeroValue = 0;
sumNumbers.sum = function(arg: number[]): number {
	let sum: number = this.zeroValue;
	let length: number = arg.length;
	for (let i: number = 0; i < length; i++) {
		sum += arg[i];
	}
	return sum;
};
let numbers: number[] = [1, 2, 3, 4, 5];
console.log(sumNumbers.sum(numbers));  // 15

クラス(SumItems)にジェネリック型を与えましたので、同じクラスにつぎのように異なる型(string)でプロパティ(zeroValue)やメソッド(sum())が定められます。


let sumStrings = new SumItems<string>();
sumStrings.zeroValue = "";
sumStrings.sum = function(arg: string[]): string {
	let sum: string = this.zeroValue;
	let length: number = arg.length;
	for (let i: number = 0; i < length; i++) {
		sum += arg[i];
	}
	return sum;
}
console.log(sumStrings.sum(['Hello', ', ', 'world', '!']));  // Hello, world!

04 ジェネリック型に継承を加える

ジェネリック型には、継承を加える("constraint"と呼ばれます)ことができます。すると、ジェネリック型のデータ(arg)から、つぎのようにインタフェース(Lengthwise)から継承したプロパティ(length)が参照できるようになるのです。


interface Lengthwise {
	length: number;
}
function getLength<T extends Lengthwise>(arg: T): number {
	return arg.length;
}
console.log(getLength<string>('hello!'));  // 6
console.log(getLength<Array<number>>([0, 1, 2]));  // 3

プロパティ(length)をもたない型(number)がジェネリック型に与えられれば、エラーになります。

Type 'number' does not satisfy the constraint 'Lengthwise'.

console.log(getLength<number>(0));  // エラー

このとき、ジェネリック型にオブジェクト型リテラルを渡すこともできます。


console.log(getLength<{length: number}>({length: 0}));

05 Handbookのコード訂正

TypeScriptサイトのHandbook「Generics」の「Using Type Parameters in Generic Constraints」には、本稿執筆時に以下のようなコードがサンプルとして掲げられています。けれど、つぎのようなエラーが示されて動きません。なお、ジェネリックの型変数は、カンマ(,)区切りで複数与えることができます。

'Type 'U[keyof U]' is not assignable to type 'T[keyof U]'. Type 'U' is not assignable to type 'T'.'

function copyFields<T extends U, U>(target: T, source: U): T {
	for (let id in source) {
		target[id] = source[id];
	}
	return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });

このコードは誤りということで、修正される予定です。意図した結果を得るには、コードはつぎのように書き替えなければなりません(keyofおよびPickについては「TypeScript 2.1」をご参照ください)。


function copyFields<T, K extends keyof T>(target: T, source: Pick<T, K>): T {
	for (let id in source) {
		target[id] = source[id];
	}
	return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(copyFields(x, { b: 10, d: 20 }));  // { a: 1, b: 10, c: 3, d: 20 }



作成者: 野中文雄
作成日: 2017年1月6日


Copyright © 2001-2017 Fumio Nonaka.  All rights reserved.