41
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Handbook を読む (6. Generics)

Last updated at Posted at 2017-03-15

TypeScript Handbook を読み進めていく第六回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics (今ココ)
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Generics

原文

Hello World of Generics

ジェネリクスの入門として、渡された引数をそのまま返す、恒等関数を実装してみましょう。

まず、ジェネリクスを使用せずに実装しようとすると、以下のように特定の型に対して実装するか、

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

any を使用することになるでしょう。

TypeScript
function identity(arg: any): any {
    return arg;
}

any を使用するとどんな型でも受け付けることができますが、戻り値として引数をそのまま返却した場合に型情報を失ってしまいます。

代わりに 型変数 を使用することで型情報を保持することができます。
以下の例では型変数 T を用いて引数の型を保持し、戻り値の型として設定しています。

TypeScript
function identity<T>(arg: T): T {
    return arg;
}
JavaScript
function identity(arg) {
    return arg;
}

インタフェースと同じく、ジェネリクスはあくまでもコンパイラのチェックに使用されるだけで、コンパイル後のコードには影響しないのね

ジェネリック関数を使用するには、型変数に渡す型を <> で囲んで指定する方法と、型推論を使用する方法の 2 通りがあります。

TypeScript
let output = identity<string>("myString");  // output の型は 'string'
TypeScript
let output = identity("myString");  // output の型は 'string'

Working with Generic Type Variables

先ほどの例の中で、型変数を指定した引数がany 型として扱われていることに気づいたかもしれません。

そのため、以下のコードはエラーになります。

TypeScript
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // エラー。T は 'length' を持つとは限らない
    return arg;
}

このコードを動作させるために、引数が任意の型の配列であることを表現しましょう。

TypeScript
function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // 配列であれば 'length' を持つため、エラーにならない
    return arg;
}

もしくは、以下のようにも書くことができます。

TypeScript
function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array は 'length' を持つため、エラーにならない
    return arg;
}

Generic Types

ジェネリック関数の型はそうでない関数の宣言と似ていますが、最初に型変数の宣言を記述する点が異なります。

TypeScript
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

<T>(arg: T) => T がジェネリック関数の型

型変数名は他の名前でも問題ありません。

TypeScript
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

また、オブジェクトリテラルのように書くことも可能です。

TypeScript
function identity<T>(arg: T): T {
    return arg;
}

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

次に、ジェネリックなインタフェースを作成してみましょう。
先ほどのオブジェクトリテラルによる宣言をインタフェース内に記述します。

TypeScript
interface GenericIdentityFn {
    <T>(arg: T): T;
}

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

let myIdentity: GenericIdentityFn = identity;

型変数をインタフェース全体に適用させることも可能です。
その場合、他のメンバでも型変数を共用できるようになります。

TypeScript
interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

また、ジェネリックなクラスを作成することも可能ですが、ジェネリックな Enum、名前空間は作成することはできません。

Generic Classes

ジェネリッククラスはジェネリックインタフェースと同じように、クラス名の後ろの角括弧 (<>) に型変数を記述します。

TypeScript
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 は数値型に限定されることはありません。
他に文字列や、より複雑なオブジェクトに対して使用することもできます。

TypeScript
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 プロパティを持つとは限らないため、使用することはできませんでした。

TypeScript
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // エラー。T 型は length プロパティを持たない
    return arg;
}

そこで、あらゆる型を受け付ける代わりに length プロパティを持つ型だけを受け付けるように制限すれば、 length プロパティを使おうとしても問題ありません。
これを実現するには length プロパティを持つインタフェースと extends キーワードを使用します。

TypeScript
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // arg が length プロパティを持っていることを知っているため、これはエラーにならない
    return arg;
}

これにより、このジェネリック関数に指定可能な型が制限されるため、任意の型に対して使用することはできなくなります。

TypeScript
loggingIdentity(3);  // エラー。数値型は length プロパティを持たない

代わりに必要なプロパティを持つ型を渡す必要があります

TypeScript
loggingIdentity({length: 10, value: 3});

Using Type Parameters in Generic Constraints

型引数を指定する時に、他の型引数に依存した型引数を指定することも可能です。

例えば、指定されたオブジェクトに存在するプロパティのみを引数に受け取りたい場合、以下のように記述します。

TypeScript
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

ジェネリック関数でファクトリを受け取る場合、型引数としてコンストラクタ関数の型を指定する必要があります。

TypeScript
function create<T>(c: {new(): T; }): T {
    return new c();
}

引数を new c() の形で使うためには、コンストラクタ関数である必要がある。となると当然引数の型はコンストラクタ関数の型になるわけだ。

コンストラクタ関数とクラスのインスタンスメンバ間の、もう少し複雑な制約の例を見てみましょう。

TypeScript
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 ということかな?

41
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?