第3章 型は便利だ楽しいな

この章ではTypeScriptの型の仕組みのうち、日常的に使う箇所を重点的に解説していきます。TypeScriptコードを書く分には使わない範囲(型定義ファイルで主に使う範囲)や複雑なものについては第4章「アドバンスド型戦略」で紹介します。

まず、TypeScriptに熟達していく上で必ず意識しなければいけない概念が型の宣言空間 (type declaration space)値の宣言空間 (variable declaration space)の存在です。別の言い方をすると、型定義と実装の差です。

JavaやC#だと、おおむね両者は密接に結びついていて、この差で困ることは少ないです。筆者が簡単に思いつく範囲では、似たような事例はJavaでのGenericsの型パラメータのtype erasureぐらいでしょうか。JavaのGenericsの型パラメータは実行時には消されてしまうため、たとえばnew T();というコードを書くことはできません。TypeScriptの型と値の区別は、Javaの例に近いかもしれません。

情報として型が存在していても、値として存在していない。そういう場面はTypeScriptではたくさん遭遇します。差を理解し積極的に活用できるようになるとTypeScript上級者といえるでしょう。

この章では、そんなTypeScriptの型の宣言空間で活躍する要素を紹介していきます。実用上のテクニックは第4章「アドバンスド型戦略」や第7章「型定義ファイルを作成する」でも言及します。

3.1 オブジェクト型リテラル(Object Type Literals)

オブジェクト型リテラルは、JavaScriptのオブジェクトリテラルに似た記法で、匿名の型を作り出す機能です(リスト3.1)。

リスト3.1: 基本的な例

// オブジェクトリテラルで値を作成!
let objA = {
  x: 1,
  y: 2,
};

// オブジェクト型リテラルで型を作成!(値は無し
// 上記の objA の型は型推論で objB と同一になる
let objB: {
  x: number;
  y: number;
};
objB = { x: 1, y: 2 };

// おんなじ!
objA = objB;
objB = objA;

export { }

値の代わりに型名を、要素の終わりに,ではなく;を書くだけです。簡単ですね。

オブジェクト型リテラルは型を指定する箇所*1であればどこでも使えます(リスト3.2)。

[*1] interfaceのextendsの後とかtypeofの後の識別子とかは厳密にいうと型を指定する箇所ではありません

リスト3.2: でも、正直読みづらい

// 関数の仮引数や返り値に対して
function move(
  value: { x: number; y: number; },
  delta: { dx?: number; dy?: number; },
): { x: number; y: number } {
  if (delta.dx) {
    value.x += delta.dx;
  }
  if (delta.dy) {
    value.y += delta.dy;
  }
  return value;
}

let result = move({ x: 1, y: 2 }, { dx: -2 });
// 次のように表示される
// {
//   "x": -1,
//   "y": 2
// }
console.log(JSON.stringify(result, null, 2));

export { }

では、オブジェクト型リテラルで使える書き方を5つ見ていきましょう。

プロパティシグニチャ(Property Signatures)

1つ目はすでに登場している、プロパティを示す記法のプロパティシグニチャです(リスト3.3)。

リスト3.3: プロパティシグニチャ

let obj: {
  property: string;
};
// 当てはまる値はこんな感じ
obj = {
  property: "Hi!",
};

export { }

素直でわかりやすいですね。

コールシグニチャ(Call Signatures)

2つ目はオブジェクトが関数として呼び出し可能であることを示す記法、コールシグニチャです(リスト3.4)。

リスト3.4: 関数として利用できる

let obj: {
  (word: string): string;
};
// 当てはまる値はこんな感じ
obj = word => `Hello, ${word}`;
obj = (word: string): string => {
  return `Hello, ${word}`;
};
obj = function(word: string): string {
  return `Hello, ${word}`;
};
// 呼び出してみよう!
let str = obj("TypeScript");
console.log(str);

export { }

コールシグニチャをつかえばオーバーロードも表現できます(リスト3.5)。

リスト3.5: オーバーロードも表現できる

let obj: {
  // overloadもできるよ
  (word: string): string;
  (): number;
};
// 当てはまる値はこんな感じ
// すべての引数と返り値に矛盾しないようにしなければならない…
obj = (word?: string): any => {
  if (typeof word === "string") {
    return `Hello, ${word}`;
  } else {
    return 42;
  }
};
// 呼び出してみよう!
let str = obj("TypeScript");
// Hello, TypeScript と表示される
console.log(str);

let num = obj();
// 42 と表示される
console.log(num);

export { }

実装が煩雑になるのでオーバーロードを自分のコード内で利用することは、なるべく避けたいところです。

コンストラクトシグニチャ(Construct Signatures)

3つ目は対象オブジェクトがコンストラクタとして利用可能であることを示す記法、コンストラクトシグニチャです(リスト3.6)。

リスト3.6: newできる

let clazz: {
  new(): any;
};

// 当てはまる値はこんな感じ
class Sample {
}
clazz = Sample;
let obj = new clazz();

// クラス式だとこんな感じ
clazz = class { };
obj = new clazz();

export { }

TypeScriptでは、クラスを定義しなければコンストラクトシグニチャにマッチするコードを書くことはできません。関数+型アサーション(要するにキャスト)を使ってanyに変換し無理やり回避する方法はありますが、使わないほうがよいでしょう。

コンストラクトシグニチャも、コールシグニチャ同様にオーバーロードが可能で、引数毎に別々の型が返るような定義も可能です。しかし、実装するのも正しく利用するのもめんどうなのでほどほどにしましょう。

コンストラクトシグニチャは主に型定義ファイルの作成時にお世話になります。

インデックスシグニチャ(Index Signatures)

4つ目は、インデックスシグニチャです。添字によるプロパティアクセスに対して、型を当てはめられます(リスト3.7)。

リスト3.7: プロパティアクセスの例

let objA: {
  [index: number]: string;
} = {};

// どういった使い方ができるの?
let s1 = objA[1];

// --noImplicitAny 付きだとちゃんとエラーになる
// error TS7015: Element implicitly has an 'any' type
//   because index expression is not of type 'number'.
// var s2 = objA["test"];

// インデックスの型指定が string の場合 string でも number でもOK
let objB: {
  [index: string]: string;
} = {};

let s3 = objB[1];
let s4 = objB["test"];

// 当てはまる値はこんな感じ
objA = {
  0: "str",
  // オブジェクトリテラルで直接変数に代入する場合、型に存在しない値があるとエラーになる
  // error TS2322: Type
  //  '{ 0: string; str: string; }'
  //     is not assignable to type '{ [index: number]: string; }'.
  //  Object literal may only specify known properties,
  //     and 'str' does not exist in type '{ [index: number]: string; }'.
  // str: "str",
};
// 変数にオブジェクトリテラル直で代入でなければ余計なパラメータがついていても許される
let tmp = {
  0: "str",
  str: "str",
};
objA = tmp;

objB = {
  0: "str",
  str: "str",
  // インデックスの型が string の場合、すべてのプロパティの型がインデックスシグニチャに
  // 反しないようにしなければならない
  // error TS2322: Type
  //  '{ 0: string; str: string; num: number; }'
  //     is not assignable to type
  //  '{ [index: string]: string; }'.
  //  Property 'num' is incompatible with index signature.
  //    Type 'number' is not assignable to type 'string'.
  // num: 1,
};

export { s1, s3, s4 }

インデックスシグニチャの型と、インデックスシグニチャ以外(たとえばプロパティシグニチャ)の型との間に矛盾が生じないようにする必要があります。

ちなみに、TypeScriptはリスト3.8のように、文字列リテラルによるアクセスも許可しています。

リスト3.8: 実はドットによるアクセスと同じ堅牢さを誇る

let obj = {
  str: "string",
  num: 1,
};

// 文字列リテラルによるアクセスだと . アクセス同様の型情報が得られる
let str: string = obj["str"];
let num: number = obj["num"];
// ちなみに、存在しない要素にアクセスすると any になる
// --noImplicitAny を使うと暗黙的anyとしてちゃんと怒られる 偉い!
// error TS7017: Index signature of object type implicitly has an 'any' type.
// let any = obj["notExists"];

// 即値じゃないとダメなので このパターンはコンパイルエラーになる
let propertyName1 = "str";
// error TS7017: Index signature of object type implicitly has an 'any' type.
// let str2 = obj[propertyName1];

// constだと型が後述の文字列リテラル型相当なのでイケる!
const propertyName2 = "str";
let str3 = obj[propertyName2];

export { str, num, propertyName1, propertyName2, str3 }

インデックスシグニチャの利用は静的な検証の恩恵を受けられない危険性が高いため、安易に使わないようにしましょう。

メソッドシグニチャ(Method Signatures)

最後の5つ目はメソッドシグニチャです。あるプロパティがメソッドであることを表現できます(リスト3.9)。

リスト3.9: メソッドの定義を示す

let obj: {
  hello(word: string): string;
};

// 当てはまる値はこんな感じ
obj = {
  hello(word: string) {
    return `Hello, ${word}`;
  },
};
obj = {
  hello: (word: string) => `Hello, ${word}`,
};
obj = {
  hello: function(word: string) {
    return `Hello, ${word}`;
  },
};

// プロパティシグニチャ + 関数型リテラル と実質同じ意味
let obj2: {
  hello: (word: string) => string;
};
obj2 = obj;
obj = obj2;

export { }

"プロパティシグニチャ+関数型リテラル(後述)"の組み合わせでも表現できますが、ぱっと見たときのわかりやすさではメソッドシグニチャのほうがよいですね。

オブジェクトリテラルと厳密なチェック

オブジェクト型リテラルの話と関わりが深いのでここでオブジェクトリテラルと厳密なチェックについて説明します。

オブジェクトリテラルを使って値を作る時に、プロパティの過不足について厳しくチェックされる場合があります。例を見てみましょう(リスト3.10)。

リスト3.10: 厳密にチェックされる場合、されない場合

// OK!変数の型に対して、過不足なし
let obj: { name: string; } = {
  name: "TypeScript",
};

// NG… 変数の型に対してプロパティが多すぎる
// error TS2322: Type '{ name: string; version: string; }'
//    is not assignable to type '{ name: string; }'.
//    Object literal may only specify known properties,
//    and 'version' does not exist in type '{ name: string; }'.
obj = {
  name: "JavaScript",
  version: "2016",
};

// オブジェクトリテラルの直接代入じゃなければOK 互換性はあるのだ
let tmp = {
  name: "JavaScript",
  version: "2016",
};
obj = tmp;

// この制約はオプション名のtypoの検出に役立つ
interface FooOptions {
  fileName?: string;
  checkBar?: boolean;
}
declare function foo(opts: FooOptions): void;

// fileName の大文字小文字を間違えている!
// Object literal may only specify known properties,
// and 'filename' does not exist in type 'FooOptions'.
foo({
  filename: "vvakame.txt",
  checkBar: true,
});

export { }

この制約はなかなか強力で、慣れないうちはコンパイルエラーを回避する方法がわからないかもしれません。型定義ファイルを使っていると、型定義ファイルに不足がある場合や時には正規の方法で攻略するのが難しい場合すらあります。

そのようなコンパイルエラーは型定義ファイルを修正して対処してほしいところですが、急いでいるのであればいったん別の変数に代入してから再代入することで回避できます。別変数作戦は、anyにキャストするやり方よりは型の不整合の検出などの点で有利なため、いくらかマシなやり方といえます。

もう一例見てみます。TypeScript 2.4系で導入された弱い型の検出 (Weak type detection)です(リスト3.11)。これは、別変数作戦を使ったときでも、特定の条件を満たす場合にコンパイルエラーとしてミスを検出できるというものです。条件は3つあります。

  1. 1つ以上プロパティを持っている
  2. すべてのプロパティがoptional
  3. インデックスシグニチャを持たない

リスト3.11: 期待されるプロパティが1つ以上必要

interface FooOptions {
  fileName?: string;
  checkBar?: boolean;
}

// 一旦別の変数を経由しても…
let obj = {
    unknownOption: 1,
};

// 共通のプロパティが1つも存在しない場合エラーにしてもらえる
// error TS2559: Type '{ unknownOption: number; }' has
//   no properties in common with type 'FooOptions'.
let fooOpts: FooOptions = obj;

これにより、まったく関係のないオブジェクトを間違って渡しているパターンを検出できます。

readonly修飾子

TypeScript固有の機能であるreadonly修飾子について紹介します。readonlyと指定したプロパティは、読み取り専用となり変更を禁止できます(リスト3.12)。

リスト3.12: readonlyで読み取り専用にする

interface Foo {
  // readonly を使うと直接は書き換えできなくなる
  readonly str: string;
}

let objA: Foo = {
  str: "TypeScript",
};
// 上書きはできない!
// error TS2450: Left-hand side of assignment expression
//   cannot be a constant or a read-only property.
// objA.str = "JavaScript";

// 別にconstではないので迂回路から変更できてしまう
let objB = {
  str: "Mutable",
};
objA = objB;
// objB経由でobjA.strを書き換える
objB.str = "Modified!";
// Modified! と表示される
console.log(objA.str);

export { }

readonlyと変更された該当箇所が変更不可になるだけなので迂回路を使うと値は変更できてしまいます。

クラスのプロパティに対して利用すると、コンストラクタでだけ値が変更可能になります(リスト3.13)。Javaのfinalに似ていますね。

リスト3.13: readonlyの変更はconstructorだけ

class Foo {
  readonly str: string;

  constructor() {
    // 変更可能
    this.str = "TypeScript";
  }

  modify() {
    // readonly が変更できるのはconstructorだけ!
    // error TS2450: Left-hand side of assignment expression
    //   cannot be a constant or a read-only property.
    // this.str = "JavaScript";
  }
}

export { Foo }

もちろん、TypeScript上の制約なのでコンパイル後のJavaScriptでは変更可能なコードです。TypeScriptコンパイラが身を守るのを助けてくれるヒント、ぐらいに捉えておきましょう。

また、getアクセサのみの実装について型定義ファイルを生成させると、これもreadonly修飾子に変換されます。

3.2 関数型リテラル(Function Type Literals)

関数型リテラルを使うと関数も型として表現できます(リスト3.14)。

リスト3.14: 関数も型として表現できる

let func: (value: string) => string;

// 当てはまる値はこんな感じ
func = word => `Hello, ${word}`;
func = (word: string) => {
  return `Hello, ${word}`;
};
func = function(word: string) {
  return `Hello, ${word}`;
};

// 型に対して実装の引数の数が少ないのはOK
func = () => "Hello, TypeScript";

// 型に対して実装の引数の数が多い場合、省略可能かデフォルト値付きでなければならない
func = (v1: string, v2 = "JavaScript") => `Hello, ${v1} & ${v2}`;

アロー関数の実装は(word: string): string => "Hello, " + word;という記法なのに対して、関数型リテラルは(word: string) => stringという記法で、返り値の型を置く位置が=>の前後という違いがあるので間違えないように気をつけましょう。

3.3 インタフェース(Interfaces)

インタフェースは多くのOOPな言語に存在しているので、ご存知の方も多いでしょう。TypeScriptのインタフェースは通常のインタフェース以上に色々な場面で登場します。TypeScriptでの基本的な使い方は名前付きオブジェクト型リテラルを作ることです。インタフェースの中で許される記法はオブジェクト型リテラルそのままです。

TypeScriptでのインタフェースの酷使されっぷりをリスト3.15で紹介します。

リスト3.15: 酷使されるインタフェースさん

// 一般的な用法
interface A {
  str: string;
}
// クラスに特定の実装を強制する
class AImpl implements A {
  str: string;
}
let objA: A = new AImpl();

// インタフェースは他のインタフェースを拡張できる
interface B1 {
  str: string;
}
interface B2 extends B1 {
  num: number;
}
// 代入する値はインタフェースを実装したクラスに限らない
let objB: B2 = {
  str: "string",
  num: 42,
};

// interfaceはクラスすら拡張できる!(実装はなかったことになる)
class FooClass {
  constructor(public num: number) {
  }
}
interface C extends FooClass {
  str: string;
}
let objC: C = {
  num: 42,
  str: "string",
};

TypeScriptでは第4章「アドバンスド型戦略」で紹介する交差型など、より多くのobject-likeな型をextendsできます。

3.4 構造的部分型(Structural Subtyping)

構造的部分型は、乱暴にいうと静的型付け用のduck typingです。TypeScriptでは、プロパティやメソッドなどの構造が一致するかどうかで型の互換性を判定します(リスト3.16)。クラスを継承しているか、インタフェースを実装しているかという厳密さはTypeScriptでは必要ありません。

リスト3.16: 構造が一緒ならまぁ一緒ってことでいいよね

// クラス Foo はstring型のstrという名前のプロパティと
// number型のnumという名前のプロパティを持つ
class Foo {
  str = "string";
  num = 1;
}

// 構造が完全に一致…!!!
// 構造が同じならもう同じってことでいいよね
let obj: Foo = {
  str: "Hi!",
  num: 42,
};

export { Foo, obj }

そのため、リスト3.17のようなコードもTypeScriptとしては正しいです。

リスト3.17: Pointインタフェースが要求されているが?

interface Point {
  x: number;
  y: number;
}

// Point の実装を強制する!!!
class PointImpl1 implements Point {
  constructor(public x: number, public y: number) {
  }
}

// Point の実装が強制されないけど互換性はある!
class PointImpl2 {
  constructor(public x: number, public y: number) {
  }
}

// 引数に Point をとる
function double(p: Point): Point {
  return {
    x: p.x * 2,
    y: p.y * 2,
  };
}

// 次の3つの関数呼び出しはすべて正しいコードです
double(new PointImpl1(1, 2));
double(new PointImpl2(3, 4));
double({
  x: 5,
  y: 6,
});

このコードはコンパイルがちゃんと成功します。Pointインタフェースに適合させることが目的のクラスであれば、きちんとimplements節を使ったほうが意図した実装だと明示できますね。

なお、省略可能なプロパティは存在していなくても型の互換性として問題ないものとして扱われます(リスト3.18)。

リスト3.18: 省略可能な(?がある)プロパティは値がなくてもよい

interface Point {
  x: number;
  y: number;
  color?: string; // なくてもいい
}
function printPoint(p: Point): void {
  let message = `x=${p.x}, y=${p.y}`;
  if (p.color) {
    message = `${p.color}(${message})`;
  }
  console.log(message);
}

// optinal なプロパティはなくても大丈夫!
printPoint({
  x: 1,
  y: 2,
});

printPoint({
  x: 1,
  y: 2,
  color: "red", // color があるパターン 型が一致していないともちろん怒られる
});

3.5 型アサーション(Type Assertions)

型アサーションは他の言語でいうところのキャストです。キャストの書き方は2種類あり、リスト3.19のように値 as 変換後型名<変換後型名>値と書くだけです。

リスト3.19: 型アサーション 基本例

let obj: any = 1;

// お前は今後 number として生きよ…
let num = obj as number;

let str = "string";
// 非常に外道なコードを書くことができる… 人としての良識を疑う
// stringをnumberな型の変数に代入するだなんてなんて卑劣な…!
num = str as any;

// 前置で書くやり方もあるよ
num = <any>str;

export { }

もちろん、互換性のない型に型アサーションで変換しようとすると怒られます(リスト3.20)。

リスト3.20: stringはnumberにはなれない

let str = "str";
// anyを経由しない場合、整合性の無い型アサーションは成功しない!安全!
// error TS2352: Type 'string' cannot be converted to type 'number'.
let num: number = str as number;

基本的に、型アサーションはなるべく使わずに済ませます。値の型を明示したい場合、値に対して型アサーションを用いるよりは変数のほうに型を明示的に注釈するほうがコンパイラのサポートの面で優れています。型アサーションより型注釈です。

ダウンキャストも実行できます(リスト3.21)。

リスト3.21: 親クラスから子クラスへ変換

class Base {
  str: string;
}

class InheritA extends Base {
  num: number;
}
class InheritB extends Base {
  bool: boolean;
}

// とりあえず型は親クラスとして…
let obj: Base = new InheritA();

// objの中身はInheritAだ!人間はそれを知っているので無理やりダウンキャスト
(obj as InheritA).num;

// なお、instanceofで調べると勝手に対象の型にダウンキャストされる(型のnarrowing)
// もちろん、キャストするよりこっちのやり方のほうが安全
if (obj instanceof InheritA) {
  obj.num;
} else if (obj instanceof InheritB) {
  obj.bool;
} else {
  obj.str;
}

export { }

TypeScriptでは自然なJavaScriptを書くと型が適切に絞り込まれるように仕組まれています。そのため、型アサーションを使った明示的な型の変換は、絶対に必要なときに限り、ひとつまみだけ使うのがよいコードです。

リスト3.22のように、anyを経由すると型を誤魔化せますが、これは一種の切り札です。型定義ファイルに不足や誤りがある場合、とりあえず誤魔化すためのスパイスとして使うことがあります。

リスト3.22: 無理やりなんとかする例

// こういう、型定義があるとする。実装は他のライブラリなどが与える。
declare class Base {
  str: string;
}

let obj = new Base();
// 本当は、Baseクラスがnumプロパティも持ってるんだけど…
// 型定義が不足しているので10秒で解決するにはanyで誤魔化すしかない
// キレイごとだけじゃ世の中生きていけないんじゃよ…
// でも、本当は型定義のほうを修正するのが正しいです
let num: number = (obj as any).num;

export { num }

3.6 ジェネリクス(Generic Types)

いよいよ来ました。最後の大ボスです。ジェネリクスはJavaなどでは総称型とも呼ばれます。

ジェネリクスなんて知らんわい!という人も、実はすでに色々なところでお世話になっています。

なお、TypeScript 2.4.1からジェネリクスが絡んだ型の互換性チェックがより正確になりました。これは破壊的変更でもあるので、過去の挙動のままにするための--noStrictGenericChecksが用意されています。もちろん、このオプションは使わないでコンパイルが通せるようにがんばるべきです。

ジェネリクスの基本

TypeScriptで一番よく使うジェネリクスを使ったクラスは、Arrayです。例を見てみましょう(リスト3.23)。

リスト3.23: 配列はジェネリクスに支えられております

// string[] は実は Array<string> と同じ意味なのだ!(Arrayだけ特別に!
let strArray: Array<string> = ["a", "b", "c"];
let numArray: Array<number> = [1, 2, 3];

// ArrayのメソッドとしてforEachがある
// forEachで渡される値の型はそれぞれ違う(forEachは汎用的だ!
strArray.forEach(v => v.charAt(0));
numArray.forEach(v => v.toFixed(2));

// 頑張ってこうやって書いてもいいけど、めんどいよね
strArray.forEach((v: string) => v.charAt(0));
numArray.forEach((v: number) => v.toFixed(2));

// あと、間違った型を指定したときにエラーにならないとこわい…
// error TS2345: Argument of type '(v: RegExp) => boolean'
//     is not assignable to parameter of type
//     '(value: string, index: number, array: string[]) => void'.
//   Types of parameters 'v' and 'value' are incompatible.
//     Type 'string' is not assignable to type 'RegExp'.
// strArray.forEach((v: RegExp) => v.test("str"));

実は、string[]という型はArray<string>と同じ意味なのです!ArrayだけはTypeScriptの中で特別扱いされています。

ここで出てくる<string>という部分がジェネリクスの肝です。Array<string>を声に出して読むと、"stringのArray"になります。ただのArrayではないのです。"stringの"という所が重要です。stringを別のものにして"numberのArray"とか"RegExpのArray"とすることもできます。つまり、色々な型に対して、"○○のArray"ということができるのです。これをプログラム上で表現するとArray<T>という表現になります。

ここで新しく出てきたT型パラメータ (type parameters)と呼びます。実際、ここで出てくるアルファベットはTでなくてもかまいせん。Typeでもいいですし、なんでもよいです。慣習として既存の型とかぶらないようにするためにアルファベット大文字1文字を使う場合が多いです。代表的な例ではTypeの頭文字のT、アルファベット的にTの次の文字でのUや、Returnの頭文字のRなどが使われます。

さて、ではTypeScriptの標準の型定義情報が書かれているlib.d.tsから抜粋したリスト3.24を見てみます。

リスト3.24: Array<T>が登場する

interface ArrayConstructor {
  new <T>(...items: T[]): T[];
};

interface Array<T> {
  length: number;
  push(...items: T[]): number;
  pop(): T | undefined;
    indexOf(searchElement: T, fromIndex?: number): number;
  [n: number]: T;
}

色々な所でTが使われています。pushの定義を見ると、"○○のArrayに対して、○○の値いくつかを追加するメソッドpush"など汎用化された要素がたくさんあります。

ここで、型パラメータTを実際にstringで具体化します(リスト3.25)。

リスト3.25: string専用Arrayに変身

interface ArrayConstructor {
  new (...items: string[]): string[];
};

interface Array {
  length: number;
  push(...items: string[]): number;
  pop(): string | undefined;
  indexOf(searchElement: string, fromIndex?: number): number;
  [n: number]: string;
}

stringのArrayに対して、stringの値をいくつか追加するメソッドpushや末尾のstringの値を1つ取得するメソッドpop、指定したstringの値と同じ要素が何番目にあるかを調べるメソッドindexOfが読み取れます。ジェネリクス、使う分にはめっちゃ簡単ですね!

このように、ジェネリクスを使うと柔軟性と堅牢さを両立させることができます。

ジェネリクスの書き方色々

ここでジェネリクスの書き方を確認しておきます(リスト3.26)。

リスト3.26: Array<T>が登場する

// それぞれのTは独立していて、関連性はない

// クラスとジェネリクス
class SampleA<T> {
  constructor(public data: T) {
  }
}
// 使い方
// 引数から推論できるので<string>は省略する場合のほうが多い
let objA = new SampleA<string>("str");

// インタフェースとジェネリクス
interface SampleB<T> {
  data: T;
}
// 使い方
let objB: SampleB<number> = { data: 1 };

// オブジェクト型リテラル各種
let obj: {
  new <T>(value: T): any;
  <T>(value: T): any;
  methodA<T>(value: T): any;
  // 型パラメータは複数あってもよい
  methodB<T, U>(value: T): U;
};

// 関数型
let func: <T>(array: T[]) => T;
// 使い方
func = <T>(array: T[]) => array[0];
func<number>([1, 2, 3]);

// コンストラクタ型
let ctor: new <T>(value: T) => any;
// 使い方
ctor = SampleA;
new ctor<string>("str");

// type alias
type SampleC<T> = { data: T; };
let objC: SampleC<number> = { data: 1 };

export { SampleA, objA, SampleB, objB, obj, SampleC, objC }

この中でよく使うのは、クラスとインタフェースとメソッドシグニチャの書き方でしょう。まずはこの3パターンの書き方を覚えておくべきです。

型パラメータと制約

型パラメータには満たすべき制約を設けることができます。例を見てみましょう(リスト3.27)。

リスト3.27: 型パラメータTはBaseを継承していなければならない

class Base {
  str: string;
}
class InheritA extends Base {
  num: number;
}

// TはBaseという制約を満たす型でなければならない
interface Sample<T extends Base> {
  method(): T;
}

// これはOK InheritAはBaseを継承している
let objA: Sample<InheritA>;

// これはダメ RegExpはBaseを継承していない
// error TS2344: Type 'RegExp' does not satisfy the constraint 'Base'.
// Property 'str' is missing in type 'RegExp'.
// let objB: Sample<RegExp>;

// これはOK 指定したオブジェクト型リテラルはBaseクラスの要件を満たす
let objC: Sample<{ str: string; }>;

interface Service<T> {
  service(t: T): T;
}

// F-Bounded Polymorphism の例
// 制約の内容に自分自身の参照を含む 極稀に使う
function f<T extends Service<T>>(x: T) {
  return x.service(x);
}

export { Base, InheritA, Sample, objA, objC, Service, f };

型パラメータが満たすべき制約をextendsの形式で指定できます。これにより、Tに何が指定されようとも、Baseに存在するプロパティには安全にアクセスできます。

型パラメータのデフォルト値

あまり使わない機能かもしれませんが、標準のPromiseの型定義にも出てくるので軽く触れておきます。ジェネリクスの型パラメータには、指定を省略し、型推論も上手にできなかった場合のデフォルトとなる型を明示できます(リスト3.28)。

リスト3.28: 型パラメータのデフォルト型をつけられる

class DataContainer<T = string> {
  data?: T

  print() {
    console.log(this.data);
  }
}

// 省略したらTはstring
const obj1 = new DataContainer();
obj1.data = "";
// defaultはstringなのでこれはダメ
// obj1.data = 1;

// 省略しなかったのでTはnumber
const obj2 = new DataContainer<number>();
obj2.data = 1;

型パラメータが2つも3つもある型を自作する場合、デフォルトの型を指定できないか考えてみるとよいでしょう。

別の活用方法として、すでにあるクラスやインタフェースなどに型パラメータを追加したい場合、関連するすべての箇所を修正する必要がありめんどくさいです。その対策として、新規に追加した型パラメータにデフォルト型を指定すると既存のコードを修正する必要がなく、安全性も損なわずに済みます。

ジェネリクスありのコードを書く

ジェネリクスを使いこなすにあたり、一番難しいことは使うことではなく、使わせることです。なぜならば、ジェネリクスを提供するコードは、何かしらの要素を抽象的なまま扱わねばならないからです。たとえば、"○○のArray"のように、型パラメータ部分が何であってもうまく動くような設計です。

逆にいうと、実際に使うときには具体化しなければならないわけで、ジェネリクス有りのコードは"必ず何かと組み合わせて具体化する"必要があります。これを使いこなすには一段上の設計力が要求されます。

自分でジェネリクスを提供するコードを作る機会は通常の範囲ではさほど多くはありません。まずはジェネリクスを適用できるような、複数の方にまたがる共通の処理を見つけ出す眼力が必要です。

3.7 オブジェクト限定型(The Object Type)

プリミティブ型となるnumber、string、boolean、symbol、null、undefined。そしてそれ以外のオブジェクト。この2種類を区別することにより防げる実行時エラーがあります。そこで導入されたのがobject型です(リスト3.29)。

リスト3.29: 非プリミティブ(オブジェクト)とプリミティブが区別できる

// object という型があると、解決できる問題がある

function receiveObject(obj: object) {
  // number, string, boolean, symbol, null, undefined は弾きたい!
  // Object.create のようにobjectとnull以外を渡すと実行時エラーになるAPIがある
  Object.create(obj);
}


// OK!
receiveObject({});

// NG! 値がprimitiveな型なので…
receiveObject(1);
receiveObject("string");
receiveObject(true);
receiveObject(1);
receiveObject(Symbol.for("symol"));
receiveObject(null);
receiveObject(undefined);

正直、自分でコードを書く上で必要になる場面はほぼないでしょう。しかしObject.createObject.setPrototypeOfObject.assignなどはプリミティブな値を渡すと実行時エラーになります。これらのエラーを早期検出するために、この工夫が必要となったわけです。単に{}を使うと、これは"プロパティを何も持たない"という意味のオブジェクト型リテラルになるため、プリミティブ型も条件を満たしてしまい、マズいのです。

3.8 "ありえない"型(The Never Type)

ありえないなんてことはありえない!はずだけれど、"ありえない"ことを示す型があります。到達不可能なコードはnever型となります。具体例を見てみましょう(リスト3.30)。

リスト3.30: "ありえない"ことが型として示される

let str = "TypeScript";
if (typeof str === "number") {
  // string型な変数の値がnumberだったら… ありえん!never!
  // error TS2339: Property 'toUpperCase' does not exist on type 'never'.
  str.toUpperCase();
}

function test(): never {
  // returnないし関数のおしりに到達できないので返り値の型はneverになる
  throw new Error();
}

let obj: never = test();
// ここに到達する事は…ありえん!
// error TS2339: Property 'test' does not exist on type 'never'.
obj.test();

export { }

変数の型がstringなのにif文ではnumberでなければ突入できない…。関数を実行すると必ず例外が発生する…。そんな、到達できないコードではnever型が利用されます。コードを書いていてnever型が必要になったり、コード上に現れることは少ないです。基本的にはnever型を見かけることがあれば、何かミスをしているな…と考えたほうがよいでしょう。