TypeScript Handbook を読み進めていく第六回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics (今ココ)
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Generics
Hello World of Generics
ジェネリクスの入門として、渡された引数をそのまま返す、恒等関数を実装してみましょう。
まず、ジェネリクスを使用せずに実装しようとすると、以下のように特定の型に対して実装するか、
function identity(arg: number): number {
return arg;
}
any
を使用することになるでしょう。
function identity(arg: any): any {
return arg;
}
any
を使用するとどんな型でも受け付けることができますが、戻り値として引数をそのまま返却した場合に型情報を失ってしまいます。
代わりに 型変数 を使用することで型情報を保持することができます。
以下の例では型変数 T
を用いて引数の型を保持し、戻り値の型として設定しています。
function identity<T>(arg: T): T {
return arg;
}
function identity(arg) {
return arg;
}
インタフェースと同じく、ジェネリクスはあくまでもコンパイラのチェックに使用されるだけで、コンパイル後のコードには影響しないのね
ジェネリック関数を使用するには、型変数に渡す型を <>
で囲んで指定する方法と、型推論を使用する方法の 2 通りがあります。
let output = identity<string>("myString"); // output の型は 'string'
let output = identity("myString"); // output の型は 'string'
Working with Generic Type Variables
先ほどの例の中で、型変数を指定した引数がany
型として扱われていることに気づいたかもしれません。
そのため、以下のコードはエラーになります。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // エラー。T は 'length' を持つとは限らない
return arg;
}
このコードを動作させるために、引数が任意の型の配列であることを表現しましょう。
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // 配列であれば 'length' を持つため、エラーにならない
return arg;
}
もしくは、以下のようにも書くことができます。
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array は 'length' を持つため、エラーにならない
return arg;
}
Generic Types
ジェネリック関数の型はそうでない関数の宣言と似ていますが、最初に型変数の宣言を記述する点が異なります。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
<T>(arg: T) => T
がジェネリック関数の型
型変数名は他の名前でも問題ありません。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
また、オブジェクトリテラルのように書くことも可能です。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
次に、ジェネリックなインタフェースを作成してみましょう。
先ほどのオブジェクトリテラルによる宣言をインタフェース内に記述します。
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
型変数をインタフェース全体に適用させることも可能です。
その場合、他のメンバでも型変数を共用できるようになります。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
また、ジェネリックなクラスを作成することも可能ですが、ジェネリックな Enum、名前空間は作成することはできません。
Generic Classes
ジェネリッククラスはジェネリックインタフェースと同じように、クラス名の後ろの角括弧 (<>
) に型変数を記述します。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
この例を見て分かるように、GenericNumber
は数値型に限定されることはありません。
他に文字列や、より複雑なオブジェクトに対して使用することもできます。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test")); // test
クラスの章 で述べたように、クラスはインスタンスメンバと静的メンバの 2 種類のメンバを持ちますが、ジェネリッククラスの型変数は静的メンバで使用することはできません。
Generic Constraints
ジェネリック関数を使う時に、型の持つメンバを使用したいと考えるかもしれません。
例えば、先ほどの loggingIdentity
の例では arg
引数の length
プロパティを使用したかったのですが、引数に指定された型が必ずしも length
プロパティを持つとは限らないため、使用することはできませんでした。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // エラー。T 型は length プロパティを持たない
return arg;
}
そこで、あらゆる型を受け付ける代わりに length
プロパティを持つ型だけを受け付けるように制限すれば、 length
プロパティを使おうとしても問題ありません。
これを実現するには length
プロパティを持つインタフェースと extends
キーワードを使用します。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // arg が length プロパティを持っていることを知っているため、これはエラーにならない
return arg;
}
これにより、このジェネリック関数に指定可能な型が制限されるため、任意の型に対して使用することはできなくなります。
loggingIdentity(3); // エラー。数値型は length プロパティを持たない
代わりに必要なプロパティを持つ型を渡す必要があります
loggingIdentity({length: 10, value: 3});
Using Type Parameters in Generic Constraints
型引数を指定する時に、他の型引数に依存した型引数を指定することも可能です。
例えば、指定されたオブジェクトに存在するプロパティのみを引数に受け取りたい場合、以下のように記述します。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // OK
getProperty(x, "m"); // エラー。'm' 型の引数は 'a' | 'b' | 'c' | 'd' 型に代入できない
説明とは直接関係ないけど、オブジェクトの持つプロパティを型として切り出せるのか。
これは TypeScript 2.1 からの新機能らしい。
Using Class Types in Generics
ジェネリック関数でファクトリを受け取る場合、型引数としてコンストラクタ関数の型を指定する必要があります。
function create<T>(c: {new(): T; }): T {
return new c();
}
引数を
new c()
の形で使うためには、コンストラクタ関数である必要がある。となると当然引数の型はコンストラクタ関数の型になるわけだ。
コンストラクタ関数とクラスのインスタンスメンバ間の、もう少し複雑な制約の例を見てみましょう。
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // 型チェック!
createInstance(Bee).keeper.hasMask; // 型チェック!
{new(): T;}
の代わりにnew () => A
でも OK ということかな?