HTML5テクニカルノート
TypeScript: ジェネリック型
- ID: FN1701002
- Technique: HTML5 / JavaScript
- Package: TypeScript 2.0
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;
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.