先の章において type system の概念を説明しましたが、そもそも type が何を意味するのかについて明確に定義をしませんでした。

Type 型とは:型とは、値の集合であり、その集合に対して実行できることの集合である。

少しわかりにくいと思うのでいくつか例を示しましょう。

  • boolean type は、全ての boolean 値(といっても二つしかないが。true と false の二つである)の集合であり、この集合に対して実行できる操作の集合である。
  • number type は全ての数値の集合であり、この集合に対して実行できる操作の集合である(例えば +, -, *, /, %, ||, &&, ?)である。これらの集合に対して実行できる操作には、.toFixed, .toPrecision, .toString といったものも含まれる。
  • string type は全ての文字列の集合であり、それに対して事項できる操作の集合である。(例えば + , || , や && ) .concat や .toUpperCase などが含まれる。

つまり、ある値の型が T であるとわかっている場合には、単にそれが T という型というだけではなく、T 型に対して「何が実行できるのか」ということも明確にわかるということです。(もちろん何ができないのかということもわかります)思い出して欲しいのですが、typechecker の肝心な役割は、あなたに invalid 無効な操作をさせないことにあります。TypeScript は valid 有効と invalid 無効を判定するために、この type を確認するのです。あなたがどの type を定義し、そしてその type をどう扱おうとするのかを把握することによって。

本章では TypeScript で使用できる Type と、その Type に対して何が実行できるのかをみていきます。

型について語るときに我々の語ること

プログラマーが型について語るとき、正確で、共有された語彙を用いて、それが一体なんなのかという概念を共有しています。このプログラマーの正確な語彙を用いて型を説明していくことにしましょう。

ある関数が、ある値を引数として受け、その値を冪乗をした値を返すとしましょう。

冪乗する関数
function squareOf (n) {
  return n * n 
} 
squareOf(2) // evaluates to 4 
squareOf('z') // evaluates to NaN 

明らかにこの関数は、number 数値を与えた場合にしか成立しません。number 以外の値をこの関数に与えた場合には、無効な結果が生じてしまいます。ですから引数の型を明示的に annotate 注釈する必要があるでしょう。

引数に annotation を追加する
function squareOf (n: number) {
  return n * n 
} 
squareOf(2) // evaluates to 4 
squareOf('z')
// Error TS2345: Argument of type '"z"' is not assignable to // parameter of type 'number'. 

こうすることで squareOf が number 以外の値を用いて呼び出された場合には、TypeScript は問題を報告すればいいのだ、ということを認識します。この例は非常に簡単なものではありますが TypeScript の type について理解するために重要な要素が十分に含まれています。このコードが指示していることは次のように整理できます。

  • squareOf の引数 n は number に限定する
  • 2 の type は、number に assignable = 割り当て可能な type である

type annotation がなければ、squareOf はその引数を制限しませんので、どんな type の引数をも渡すことができてしまいます。しかし一度制限を加えれば、TypeScript はこの関数を呼び出す場所すべてを探し出し、適合した引数で呼び出しているかどうかを確認します。今回の例で言えば、2 の type は number ですので、squareOf の annotation に指定した number に適合しますから、TypeScript はこのコードを承認するのです。しかし、'z' の type は string であり type string には割り当て不可能ですので、TypeScript は問題報告をします。

この性質に関しては、数学用語の bounds = 境界の概念を用いて考えることもできるでしょう。TypeScript への指示として、 n の upper bound = 上界を number と定義すれば、squareOf を通行できる値は少なくとも number に含まれる集合でなくてはいけません。もし number に含まれる集合ではない場合、例えば number もしくは string という集合の場合には、これは n には適合しないということになります。

assignability = 適合可能性, bounds = 境界, constraints = 制限に関しては正式に第6章で定義をするつもりです。今の所は「要求した特定の type が、要求した場所において、適切に使われているかどうか」ということを意味しているということだけ理解していただければと思います。

Type のいろは

では TypeScript が扱える type について探求をしていきましょう。つまりどんな type にどんな値が含まれるのか、そしてどんな type に対してどんな操作ができるのかをみていきます。また type に扱うにあたって必要になる基礎的な言語機能である type alias, union types, intersection types といった要素もみていきましょう。

any

any は type の親分ともいうべき存在です。any を使うのに面倒な手続きは一切必要ありませんが、しかしこれを使うしかないという状況に追い込まれない限りは、any を使いたいと思うことはないでしょう。TypeScript においては、コンパイル時に少なくともなんらかの type を持つ必要があるのですが、プログラマーが何も type を指定せず、そして typechecker も type が何であるのか推測できなかった場合に、any が使われます。つまり any は最後の手段であり、できる限り避けるべき type なのです。

なぜ any を使うのはなるべく避けるべきなのでしょうか?Type とは何だったのか思い出して欲しいのです。(type は値の集合であり、その集合にあなたが実行できることでした)any は全ての値の集合であり、any に対しては「どんなことでも」実行できてしまいます。これが意味するのは、type any の値が存在するということは、その値に対して、加算も乗算も .pizza() を実行することもできるということです。全てが実行できるというのはそういうことを意味します。

any を使うということは、その値を通常の JavaScript のように扱うということであり、結果として TypeScript の typechecker の仕事を完全に妨害するということでもあります。any をコードに使うということは、すべてを投げうってしまうということです。any は本当に本当の最後の手段と考えてください。

TypeScript がある値を type any と推測した場合には(例えば関数の引数に annotation をつけ忘れた場合や、型定義がされていない JavaScript ライブラリを import した場合に起きます)コンパイル時に exeption を投げますし、エディターに赤い波線が表示されます。ただし明示的に any を付与した場合には、この exeption は起きません。なぜなら「それがあなたの望んだこと」なのですから…

メモ:デフォルトでは any を暗黙的に推測したとしても、それを許可する設定になっています。暗黙的な any を防ぐためには noImplicitAny フラッグを tscinfug.json で有効にしてください。

noImplicitAny は strict の一部に含まれるので strict を有効にしていればそれでも OK です。

unknown

any が悪の親玉だとしたら、unknown は Point break に登場する FBI 潜入捜査官、キアヌリーブス演じるジョニー・ユタのような存在です。けだるい雰囲気で、いかにもチンピラという印象ですが、その奥底には「秩序」への敬意を持った、いい人なのです。あまりこういうことがあってはいけませんが、もし値の型が今のところ判然としない場合には、any ではなく、unknown に手を伸ばすようにしましょう。unknown は any と同じく全ての値を表していますが、refinement によってその型が狭められるまでは、後述する操作を除くほとんどの操作を受け付けません。

では unknown type が受け付ける操作とは何でしょうか。unknown を ==, ===, ||, &&, ? を使って値と比較すること、それから ! を使って値を反転させること、そして JavaScript の typeof もしくは instanceof オペレーターを使って refine することです。つまりこんなふうに unknown を使うことができます。

unknown の使い方
let a: unknown = 30; // unknown
let b = a === 123; // boolean
let c = a + 10; // Error TS2571: Object is of type 'unknown'.
if (typeof a === "number") {
  let d = a + 10;
  // number
}

このコードサンプルが示す概念を以下にまとめました。

  • TypeScript が推測によって unknown type を与えることはない。用いたい場合には明確にあなたが指定する必要がある。(a)
  • 値と unknown の値の二つを比較することはできる。(b)
  • しかし unknown な値が、何か特定の type をもっているかのように操作をすることはできない。(c)もし何らかの操作をしたいのであれば、TypeScript に対してそれが本当にある特定の type であることをまず示さなければならない。

boolean

Boolean type には二つの値があります。true と false です。boolean に対してできる操作は次のものです。unknown を ==, ===, ||, &&, ? を使って値と比較すること、それから ! を使って値を反転させること。それだけです。

boolean type
let a = true; // boolean
var b = false; // boolean
const c = true; // true
let d: boolean = true; // boolean
let e: true = true; // true
let f: true = false;
// Error TS2322: Type 'false' is not assignable
// to type 'true'.

上記の例は TypeScript に値が boolean type であると伝える方法の概要を示しています。

  • TypeScript に推測させる(a と b)
  • TypeScript に特定の boolean 値であることを推測させる(c)
  • TypeScript に boolean type であることを示す(d)
  • TypeScript に特定の boolean 値 であることを示す(e,f)

一般的にいって最初の二つを使うことがほとんどでしょう。非常にレアケースですが四番目の方法を使うこともあります。こうすることでより絞り込んだ type safe を与えています。三番目の方法を使うことはほとんどないといっていいでしょう。

二番目と四番目のケースは、直感的に何をやっているかは明らかであるものの、しかし非常に注目に値するものといっていいでしょう。なぜならこの機能を持ったプログラミング言語というのは非常に限られており、それゆえあなたにとっても見慣れないものであるだろうからです。ここで TypeScript に伝えているのは次のことです。「いいかい TypeScirpt、ここに e という値があるね。この e は昔ながらの boolean じゃないんだ。boolean の中でも true だけを表している」value を type に用いることで、e と f に許容される値を boolean 全てではなく、特定の boolean 値に限定しているのです。この機能は type literals というものです。

Type Literal:特定の単一の value だけを表す type のこと。

四番目のケースで私は明示的に変数に対して type literals で annotation を与えました。二番目のケースでは TypeScript に type literals であると推測させました。そうなったのは let や var ではなく、const を用いたからです。こうすることで TypeScript は、const でアサインされた絶対に変更されない primitive 値であるめに、その変数にとって一番狭められた type を推測することができるのです。そういうわけで、二番目のケースは c の type を boolean ではなく true と推測したのです。let を用いた場合と const を用いた場合でことを推測する理由については後述します。

Type literals については本書を通して何度か扱っていきます。この機能はさらなる type safety を引き出す非常に強力な機能です。type literals は言語的に TypeScript を独自なものにしており、この点は Java を使っているあなたの友人に自慢できるのではないでしょうか?

number

number は全ての数値の集合である。これには integers 整数, floats 浮動小数, positives 正数, negatives 負数, Infinity 無限, NaN = not a number といったものが含まれている。number に対してできる処理は、数値に対してできる処理一般であり、つまり +, -, %, そして > を用いた比較である。いくつか例をみてみましょう。

type number への操作
let a = 1234; // number
var b = Infinity * 0.1; // number
const c = 5678; // 5678
let d = a < b; // boolean
let e: number = 100; // number
let f: 26.218 = 26.218; // 26.218
let g: 26.218 = 10;
// Error TS2322: Type '10' is not assignable
// to type '26.218'.

boolean type の例と同じく、number type を付与する方法には 4 つの方法があります。

  • TypeScript に値に対して推測をさせ、number type を付与する(a, b)
  • const を用いることで TypeScript に特定の数値 number であると推測させる(c)
  • 明示的に値が number であることを TypeScript に指示する(e)
  • 明示的に特定の数値 number であることを TypeScript に伝える(f, g)

boolean の場合と同じく let を使うことで TypeScript に number type を推測させることができます。(第一の方法)あなたが賢いプログラマーであれば、さらに number type を絞って特定の数値に限定することもできます。(二番目と四番目の方法) 三番目の方法のように number type であることをわざわざ宣言する合理的な理由はありません。

ちょっとしたアドバイスメモ:数字が大きい場合、numeric separator を使うことで可読性を向上させることができます。type annotation の位置でも、値の宣言の位置でも使うことができます。

数字が大きい場合の書き方
let oneMillion = 1_000_000 
// Equivalent to 1000000 
let twoMillion : 2_000_000 = 2_000_000 

bigint

bigint は JavaScript と TypeScript の両方にとって新人です。これを用いることで rounding error が生じることなく大きい数値を扱うことができます。number は最大で 2 の 53 乗 までしか扱うことができないのですが、bigint はそれ以上も扱うことができます。bigint type は全ての bigint の集合であり、+, -, *, /, < 等を用いることができます。

bigint
let a = 1234n // bigint 
const b = 5678n // 5678n 
var c = a + b // bigint 
let d = a < 1235 // boolean 
let e = 88.5n // Error TS1353: A bigint literal must be an integer. 
let f : bigint = 100n // bigint 
let g : 100n = 100n // 100n 
let h : bigint = 100 
// Error TS2322: Type '100' is not assignable 
// to type 'bigint'. 

boolen や number と同じく、4つの方法があります。実際に試してみてください。

注意:現時点では全ての JavaScript エンジンで bigint がサポートされているわけではありません。bigint を用いる場合には、対象のプラットフォームでサポートされているかどうかをしっかり確認しましょう。

string

string は全ての string 文字列の集合であり、それに対して実行できる「結合 +, スライス(.slice())」といった処理の集合です。

string type
let a = "hello"; // string
var b = "billy"; // string
const c = "!"; // '!'
let d = a + " " + b + c; // string
let e: string = "zoom"; // string
let f: "john" = "john"; // 'john'
let g: "john" = "zoe";
// Error TS2322: Type "zoe" is not assignable
// to type "john".

symbol

symbol は比較的新しい言語機能であり、直近の一番大きなメジャーバージョンである ES2015 で追加されました。Symbol はあまり頻繁に実際のコードに出てくることはありませんが、以下のような特定の目的で使用されます。つまりobject や map における string key の代替として使われ、さらに確実に把握できている key でしかアクセスできず、たまたま一致する key でアクセスできないようにするためです。

symbol type
let a = Symbol("a"); // symbol
let b: symbol = Symbol("b"); // symbol
var c = a === b; // boolean
let d = a + "x";
// Error TS2469: The '+' operator cannot be applied
// to type 'symbol'.

Symbol('a') とすることで与えられた名称(ここでは 'a')を持った symbol を新しく作成します。symbol は unique なので、決して他の symbol と比較した場合に一致することはありません。例え同じ名称の symbol 同士であってもです。また、number の推測をさせるにあたって let と const を使った場合にそれぞれ number type と特定の value の number type と異なる結果になったように、symbol においても同様の結果がおきます。const を使って symbole を宣言すると、その型はさらに絞り込まれ unique symbol となります。

symbol 2
const e = Symbol("e"); // typeof e
const f: unique symbol = Symbol("f"); // typeof f
let g: unique symbol = Symbol("f");
// Error TS1332: A variable whose type is a
// 'unique symbol' type must be 'const'.
let h = e === e; // boolean
let i = e === f;
// Error TS2367: This condition will always return
// 'false' since the types 'unique symbol' and
// 'unique symbol' have no overlap.
  • 新しい symbol を宣言しその際に const を使った場合には(let や var ではダメです)TypeScript は unique symbol type であると推測する。ただしエディターには unique symbol とは表示されず、type が変数名で表示される。
  • const を用いる場合には、明示的に unique symbol type を与えることができる。
  • unique symbol は常に自分自身としか一致しない。
  • TypeScript は unique symbol はその他の unique symbol とは絶対に一致しないことを把握している。

unique symbol を他の literal type と同様に考えることができるでしょう。1 や true や "literal" と同様にです。

object

TypeScript の object type は、object の shape = 形状を決定します。特筆すべきは単純な object({} を使って生成する object) とより複雑な object(new Blah という形で生成する object) を区別しないことです。我々の言語が、そういう設計だからです。つまり JavaScript は generally structurally typed なので、TypeScript は nominally typed style よりも、こういったプログラミングスタイルを好むのです。

Structural typing とは:object が特定のプロパティを持つかどうかを気にするだけで、object や class の名称が一致するかどうか(nominal typing は気にします)は気にしないというプログラミングスタイルです。duck typing と呼ばれることもあります。

object のための type を TypeScript において表現する方法は複数あります。最初の方法は、値を object として宣言することです。

object
let a: object = { b: "x" };
// What happens when you access b ?
a.b; // Error TS2339: Property 'b' does not exist on type 'object'.

ちょっとちょっと、何の役にも立たないじゃないか…object type には何もできないっていうんでしょうか?

さあ熟練の TypeScript エンジニアになるために大事なポイントですよ。object type は any type よりも制限が厳しいわけですが、実はかなり厳しいのです。object type は単にその値が object であること以外には何も伝えてくれません。(せいぜい null 出ないことくらいしかわかりません)

明示的な annotation をやめ、TypeScript に推測させた場合にはどうなるでしょうか。

object
let a = {
  b: "x"
}; // {b: string}

a.b; // string

let b = {
  c: {
    d: "f"
  }
}; // {c: {d: string}}

注意してご覧ください!object type を宣言する二つ目の方法を見つけてしまいましたね。そう object literal syntax です。上記の例のように let を使って TypeScript にオブジェクトの形状 = shape を推測させることもできますし、次のように curly brace({}) の中に明示的に表現することもできます。

object
let a: { b: number } = {
  b: 12
}; // {b: number}

Object を const で宣言した場合の type 推測について:let ではなく const を用いて object を宣言した場合にはどうなるのでしょうか?

object
let a: { b: number } = {
  b: 12
}; // Still {b: number}

TypeScript が b を number と推測したことは意外に思うかもしれません。literal で 12 になるのかなと思いますよね。なぜなら number や string の場合には、const で宣言するのか let で宣言するのかが TypeScript の type 推測に影響を与えたからです。

今まで見てきた boolean, number, bigint, string, symbol といった primitive type とは異なり、const を用いて object を宣言しても、TypeScript はその type をよりせばめようとはしてくれないのです。なぜならば JavaScript の object は mutable なので、object を作った後にもその field は後で更新される可能性がある、と TypeScript はしっかり理解しているのです。

この概念については後ほどより詳細に取り上げることとします。

Object literal シンタックスは次のように TypeScript に伝えます。「ここに、こういった shape 形状のものがあるね。これは object literal であるかもしくは class だ。」

object literal syntax
let c: {
  firstName: string;
  lastName: string;
} = {
  firstName: "john",
  lastName: "barrowman"
};

class Person {
  constructor(
    public firstName: string,
    // public is shorthand for
    // this.firstName = firstName
    public lastName: string
  ) {}
}
c = new Person("matt", "smith"); // OK

{firstName: string, lastName: string} はこの object の shape を表しており、object literal でも Person class の class instance のどちらもこの shape を満たしているので、Person インスタンスを c にアサインすることを TypeScript は許可するのです。

さらに探求を深めるために、余分なプロパティを持たせたり、反対に必要なプロパティを除外するとどうなるか試してみましょう。

object deep
let a: { b: number };
a = {}; 
// Error TS2741: Property 'b' is missing in type '{}'
// but required in type '{b: number}'.

a = { b: 1, c: 2 };
// Error TS2322: Type '{b: number; c: number}' is not assignable }
// to type '{b: number}'. Object literal may only specify known
// properties, and 'c' does not exist in type '{b: number}'.

初期設定では TypeScript は object の property に関しては厳密な態度を取ります。あなたがある object が b というプロパティを持ちそれが number であると指示をすれば、TypeScript は b を期待しますし、同時に b 以外が存在しないことを期待します。b がない場合や、もしくは b 以外の余分なプロパティがある場合には TypeScript は問題を報告します。

では TypeScript にたいしてある要素が optional であることを伝えるにはどうしたらいいのでしょうか。さらに他にも追加予定のプロパティがあることを示すには?

optional
let a: {
  b: number; // (1)
  c?: string; // (2)
  [key: number]: boolean; // (3)
};
  • 1: a は be というプロパティを持ちそれは number である
  • 2: a は c というプロパティを持つ可能性があり、その場合 c は string である。もしくは c に対して undefiled を当てることもできる。
  • 3: a は数値をキーにしたプロパティを持つ可能性があり、その場合値は boolean type である。

ではこの type を持つ object に対して何がアサインできるかみていきましょう。

アサインしてみる
let a: {
  b: number;
  c?: string;
  [key: number]: boolean;
};

a = { b: 1 };
a = { b: 1, c: undefined };
a = { b: 1, c: "d" };

a = { b: 1, 10: true };
a = { b: 1, 10: true, 20: false };
a = { 10: true };
// Error TS2741: Property 'b' is missing in type
// '{10: true}'.

a = { b: 1, 33: "red" };
// Error TS2741: Type 'string' is not assignable
// to type 'boolean'.

Index signatures について:[key: T]:U という形式のシンタックスは、index sigature と呼ばれるもので、これを用いることで TypeScript にたいして指定の object が複数の key を持つことを宣言できます。これが示しているのは「このオブジェクトは、T type の key に対して U type の value を持たなければいけない」ということです。index signature はオブジェクトに追加のキーを与える場合にも type safe を保証することができます。もちろん明示的に宣言した key に加えてです。

index signature について留意してほしいことが一つあります。それは index signature の key type である (T) は、number もしくは string ではなくてはいけない、ということです。また key 名称は必ずしも key である必要はありません。

index signature
let airplaneSeatingAssignments: { [seatNumber: string]: string } = {
  "34D": "Boris Cherny",
  "34E": "Bill Gates"
};

Optional(?) 以外にもオブジェクトの type 宣言に使用できるものがあります。それは readonly です。これをオブジェクトのフィールドに追記することで、そのフィールドは変数の初期化以降に変更することができなくなります。これはオブジェクトのプロパティに対する const 宣言だと考えることができるでしょう。

readonly
let user: {
  readonly firstName: string;
} = {
  firstName: "abby"
};

user.firstName; // string user .
firstName = "abbey with an e"; 
// Error TS2540: Cannot assign to 'firstName' because it 
// is a read-only property.

Object literal を用いた notation には一つだけ特殊な状態があります。からのオブジェクトリテラル({})を割り当てた場合です。この場合、null と undefined を除いた全ての type をアサイン可能なのです。これは非常に予測しづらい挙動ですので、このような使い方は最大限取り除くようにするべきです。

danger case
let danger: {};
danger = {};
danger = { x: 1 };
danger = [];
danger = 2;

最後にもう一つだけ。Object を使って type object を宣言する方法です。これはほとんど {} を使った場合と同じで、これも最大限避けなければいけません。

では object type を宣言する方法をまとめましょう。

  • object lieral を用いた notatiton で shape とも呼ばれる。(例えば {a: string} など)これはオブジェクトが持つべきフィールドが明確にわかっている場合と、全てのオブジェクトの値が同じ type の場合に用いる。
  • 空の object literal {} を用いた宣言。これは最大限避けるべきだ。
  • object type を用いた宣言。object type であること以外には、どんなフィールドを持っているかなどを全く気にしない場合にのみ使用する。
  • Object type を使った宣言。避けるべき。

常に一番目か三番目のみを用いるべきでしょう。二番目と四番目の手法に関してはもいる場合にはかなりの注意が必要です。これが使われている場所をリンターによって忠告し、コードレビューでも問題を指摘し、そして使う必要はないと書かれたポスターをオフィスに貼ったほうがいいでしょう。これらの手法を取り除くために、最大限ツールを活用してください。

閑話休題:Type Alias, Union, Intersection

あなたは着々と老練な TypeScript プログラマーになりつつある。いくつかの type とそれがどのように機能するかをみてきた。いまや type system が何なのか、そして type とは、type safety が何なのかということについては、かなり親しみを覚える概念となったはずだ。さあ、深淵に足を踏み入れる時だ。

ご存知の通り、ある値に対しては、type の許可に基づき、特定の操作を行うことができましたね。例えば + を二つの number を加えるために用いることや、.toUpperCase を文字列を大文字にするために用いることができます。

さらに踏み込んで、ある type に対して操作を加えることもできるのです。ここでは少しだけ type-level の操作を紹介することにしましょう。他にもたくさんこの本の後半で取り上げるのですが、今取り上げようとしている操作に関してはとても一般的なものなので、なるべく早く紹介すべきだと思ったからです。

Type alias

変数宣言を用いて、値に別名を与える(= alias)変数を宣言できるように、type aliase を用いることで、特定の type への参照をさせることができます。

type alias
type Age = number;
type Person = {
  name: string;
  age: Age;
};

Age は純粋に number でしかありませんが、こうすることで Person の shape が理解しやすくなります。Alias は TypeScript によって推測されることは決してないので明示的に宣言する必要があります。

type alias
let age: Age = 55;
let driver: Person = {
  name: "James May",
  age: age
};

Age は単に number type への alias でしかないので、これが示しているのは number がアサインできるというだけです。ですから次のように書くこともできます。

type alias
let age = 55;
let driver: Person = {
  name: "James May",
  age: age
};

Type alias は type に変えることができます。そうしてもコードの意味は全く変わりません。

JavaScript の変数宣言と同じく、type alias は二度宣言することはできません。

type alias
type Color = "red";
type Color = "blue";
// Error TS2300: Duplicate identifier 'Color'.

let や const と同じく type alias はブロックスコープです。なのでブロックの内部で type alias を宣言した場合には、それより外のスコープの type alias を隠してしまいます。

scope and shadow
type Color = "red";
let x = Math.random() < 0.5;
if (x) {
  type Color = "blue";
  // This shadows the Color declared above.
  let b: Color = "blue";
} else {
  let c: Color = "red";
}

Type alias を用いることで複雑な type を繰り返し指定する必要は無くなります。またある変数が何のために使われているのかも明確になります。type alias を用いるかどうかの判断基準としては、ある値を変数に入れるか入れないかと同じような判断基準を type alias に対しても用いることができるでしょう。

union type と intersection type

A と B の二つの集合があった場合、この二つの union は、合算 sum になります。(つまり A もしくは B もしくは A かつ B)それに対してこの二つの intersection は両者の共通部分になります。(つまり A かつ B の部分)Type における union と interserction は集合論のそれと完全に一致します。(中西注:ここで union と intersection の図を用意する)

TypeScript には type の union と intersection を作り出すための operator が用意されています。union のためには | を、intersrction のためには & を用いることができます。

union and intersection
type Cat = { name: string; purrs: boolean };
type Dog = { name: string; barks: boolean; wags: boolean };
type CatOrDogOrBoth = Cat | Dog;
type CatAndDog = Cat & Dog;

type が CatOrDogOrBoth であるとわかった場合に、そこからどんな情報が得られるでしょうか。name プロパティを持ち、値が string であること以外には、ほとんど何もわかりません。では CatOrDogOrBoth にアサイン可能なものは?そうです。Cat か Dog かもしくはその両方です。

union and intersection
// Cat
let a: CatOrDogOrBoth = {
  name: "Bonkers",
  purrs: true
};

// Dog
a = {
  name: "Domino",
  barks: true,
  wags: true
};

// Both
a = {
  name: "Donkers",
  barks: true,
  purrs: true,
  wags: true
};

union type(|) の値は、必ずしもどちらかの(Cat か Dog)のプロパティでなくてはいけないのではありません。両者のプロパティを同時に持つこともできるのです。

intersection type の場合はどうでしょうか。この犬のような猫のような超越的愛玩動物は、name プロパティを持つだけではなく、猫なで声で、吠え、尻尾を振ります。

intersection
let b: CatAndDog = {
  name: "Domino",
  barks: true,
  purrs: true,
  wags: true
};

Union は大抵の場合 intersection よりも自然な結果をもたらします。次の関数を例にとってみましょう。

example
function trueOrNull(isTrue: boolean) {
  if (isTrue) {
    return "true";
  }
  return null;
}

この関数が返す値の type はなんでしょうか?string もしくは null ですね。この返り値の type は以下のように定義することができます。

example
type Returns = string | null 

では次のような場合は?

example
function someFunc(a: string, b: number) {
  return a || b;
}

a が true と評価された場合には string を返しますし、そうではない場合には number を返します。つまり string | number と表現することができます。

この union type の返り値が発生しがちな、しかしそうであるべきではない機会は、array を用いる場合です(しかも配列の要素が均質ではない場合は特に)が、これはまた次の機会に話すことにしましょう。

Arrays

JavaScript と同じく TypeScript の配列はある特定の機能を持ったオブジェクトであり、concat, push, search, slice といった操作をサポートしています。

arrays
let a = [1, 2, 3]; // number[]
var b = ["a", "b"]; // string[]
let c: string[] = ["a"]; // string[]
let d = [1, "a"]; // (string | number)[]
const e = [2, "b"]; // (string | number)[]
let f = ["red"];
f.push("blue");
f.push(true); 
// Error TS2345: Argument of type 'true' is not
// assignable to parameter of type 'string'.
let g = []; // any[]
g.push(1); // number[]
g.push("red"); // (string | number)[]
let h: number[] = []; // number[]
h.push(1); // number[]
h.push("red"); 
// Error TS2345: Argument of type '"red"' is not
// assignable to parameter of type 'number'.

ちょっとしたアドバイス:TypeScript は二種類の array シンタックスをサポートしています。T[]Array<T> です。この二つは意味においてもパフォーマンスの性能においても全く同じです。本書では T[] 形式を用いますが、基本的にはどちらでも構いません。

これらの例の中で明示的に type を指定しているのは c と h だけです。また array に対して TypeScript が何を許可して、何を許可しないのかもおそらく理解できたでしょう。

TypeScript において array を運用するさいに、一般的に望ましい守るベキールがあります。それは、配列の中身を「均質」に保つことです。つまり、一つの配列の中に、りんごとオレンジと数値をごちゃまぜに入れないようにしてくれ、ということです。配列の中身が全て、同じ type にすればいいのです。その理由は、もし均質にしなかった場合には、TypeScript に type safe であることを証明するための作業が増えてしまい、面倒になるからです。

配列の中身が均質である場合に何故作業が容易になるのか、f の例をみながら説明しましょう。f の場合は配列を string である 'red'によって初期化しました。(ここで重要なのは、array に string だけを持たせて宣言した場合、TypeScript は配列の中身は必ず string になると推測するという点です)この配列に 'blue' を push すると、'blue' は文字列ですので、TypeScript はこれを許可します。しかし、true を push しようとすると失敗します。何故なら f は string を持つ配列であり、ture は string ではないため TypeScript がそれを許可しないからです。

別の例をみてみましょう。d を number と string によって初期化しました。ですから TypeScript は number | string を持つ配列だと推測します。この配列の要素は number もしくは string ですから、これを用いる場合にはどちらの type なのかを確認する作業が必要になります。例えばこの配列にたいして map を用いて、string の場合には大文字に変換し、number の場合には三倍するとしましょう。

map を使う
let d = [1, "a"];
d.map(_ => {
  if (typeof _ === "number") {
    return _ * 3;
  }
  return _.toUpperCase();
});

つまり全ての要素にたいして typeof を用いて number なのか string なのかをチェックして初めて、それに対して操作が行えるわけです。

オブジェクトの場合と同じく const を用いて配列を作成しても、その type 推測を狭めることはできません。そのために d と e のサンプルそれぞれが number | string の配列であると推測されるわけです。

g は特殊なケースです。空配列によって array を初期化した場合には、TypeScript は配列の要素の型がどのようなものになるのか全く判断することができないので、any と 推測します。しかしこの配列に操作を加え、要素を加える度に、TypeScript は、その要素の type をこの配列が持ち得る type に加えます。一旦 array が定義されたスコープから外れた場合には(例えば関数の中で array を定義し、その array を return した時点で)TypeScript は最終的に決定されたその type をアサインし、それ以上拡張することはできなくなります。

array
function buildArray() {
  let a = [];
  // any[]
  a.push(1); // number[]
  a.push("x"); // (string | number)[]
  return a;
}

let myArray = buildArray(); // (string | number)[]
myArray.push(true);
// Error 2345: Argument of type 'true' is not
// assignable to parameter of type 'string | number'.

Tuples

Tuple は array の subtype です。tuple を用いることで、特定の長さで、かつ各 index にたいして特定の type を持った配列を作ることができます。ほかの type とは異なり、tuple に関しては明示的に type を指定せねばいけません。何故なら、tuple と array のシンタックスが JavaScript においては全く同じだからです。そのため TypeScript には tuple を指定するための特別なシンタックスが用意されています。

tuples
let a: [number] = [1];

// A tuple of [first name, last name, birth year]
let b: [string, string, number] = ["malcolm", "gladwell", 1963];
b = ["queen", "elizabeth", "ii", 1926];
// Error TS2322: Type 'string' is not
// assignable to type 'number'.

tuple は optional element もサポートしています。object type の場合と同じく ? を使うことで optional であることを示します。

array optional
// An array of train fares, which sometimes vary depending on direction
let trainFares: [number, number?][] = [[3.75], [8.25, 7.7], [10.5]];
// Equivalently:
let moreTrainFares: ([number] | [number, number])[] = [
  //...
];

tuples は rest elements もサポートしており、これにより最小の長さを指定した tuple type を指定することができます。

tuple rest elements
// A list of strings with at least 1 element
let friends: [string, ...string[]] = ["Sara", "Tali", "Chloe", "Claire"];
// A heterogeneous list
let list: [number, boolean, ...string[]] = [1, false, "a", "b", "c"];

tuple type を用いることによって、内容が異なるリスト heterogeneous list に type safe をもたらすだけではなく、長さや、各 index ごとの type を指定することができます。これによって旧来の単なる array よりも圧倒的な type safe を得ることができますので、多用した方がいいでしょう。

read-only arrays and tuples

通常の array は mutable ですが(mutable とはつまり .push() や .splice 等を用いれば array の中身を変更できるということです)、immutable な array を用いたい場合もあるでしょう。つまり、中身を更新したい場合には、新たな array を作成して、元の array には変更を一切加えない手法です。

TypeScript は readonly array type を用意していますので、これを用いて immutable な array を作成することができます。Read-only array は通常の array とほとんど同じですが、唯一違うのは、その中身を更新することができない点です。read-only array を作るためには、明確に type annotation を付与する必要があります。read-only array を用いてるさいに更新を行いたい場合は。.concat, .slice といったメソッドを使いましょう。

readonly arrays
let as : readonly number [] = [ 1 , 2 , 3 ] // readonly number[]
let bs : readonly number [] = as . concat ( 4 ) // readonly number[]
let three = bs [ 2 ] // number
as[4] = 5; // Error TS2542: Index signature in type // 'readonly number[]' only permits reading.
as.push(6); // Error TS2339: Property 'push' does not // exist on type 'readonly number[]'.

他にも次のような宣言方法があります。もちろん tuple を readonly にすることも可能です。どの宣言方法を用いるかはあなたの好みで選んで構いません。

readonly arrays
type A = readonly string[]; // readonly string[]
type B = ReadonlyArray<string>; // readonly string[]
type C = Readonly<string[]>; // readonly string[]
type D = readonly [number, string]; // readonly [number, string]
type E = Readonly<[number, string]>; // readonly [number, string]

read-only array は mutability を避けることによってコードを理解しやすくしますが、依然として通常の JavaScript array の仕組みに依拠している天に注意してください。つまりどんなに小さな変更であっても、まず最初にコピーを作成しているので、もし注意深く使用しない場合に、アプリケーションのランタイムパフォーマンスを下げる可能性があります。小さな配列にとってはこの overhead は認識できるほど大きなものではありませんが、しかし大きな配列にとってはオーバーヘッドがそれなりに大きなものになってしまう可能性はあります。

null, undefined, void, and never

JavaScript は何も存在しないことを示す値として二つの値があります。null と undefined です。TypeScript はもちろんこの二つの値をサポートしていますし、type をサポートしています。ご察しの通り null と undefined という type です。

undefined type の値は undefined だけで、null type の値も null だけです。

JavaScript プログラマーは null と undefined を区別して使わない傾向にありますが、少しだけ意味に違いがあります。undefined は、まだ定義されていないということを意味し、null は値が存在しないことを意味します。(例えば値を算出しようとしたが、途中でエラーが出たような場合です)これは一般的な監修というべきものであって、TypeScirpt があなたのチームにそうすることを要求するわけではありませんが、この区別をすることは有用でしょう。

null と undefined 以外にも、 void と never という似たような type があります。この二つは、存在しない系の type の中でもかなり繊細な違いを示すためにあります。void は 関数が明示的には何も返さない場合に返り値の type として用いることができます。(例えば console.log を実行した場合など)そして never は関数が全く何も返さない場合に用いることができます。(例えば exception を投げたり、無限に実行されるような関数等)

null,undefined,void,never
// (a) A function that returns a number or null
function a(x: number) {
  if (x < 10) {
    return x;
  }
  return null;
}

// (b) A function that returns undefined
function b() {
  return undefined;
}

// (c) A function that returns void
function c() {
  let a = 2 + 2;
  let b = a * a;
}

// (d) A function that returns never
function d() {
  throw TypeError("I always error");
}

// (e) Another function that returns never
function e() {
  while (true) {
    doSomething();
  }
}

a と b は明示的に null と undefined を返しています。c は undefined を返しますが、明示的に return しているわけではありません。こういう場合には void を使います。d は exeption を投げ、e は無限に実行されるので、それぞれ決して return はされません。こういう場合は never を用います。

unknown は全ての type の supertype といえますが、反対に never は全ての type の subtype です。こういう場合、bottom type と呼ぶことができます。bottom type が意味するのは、全ての type にアサイン可能であり、同時に bottom type の値は決して type safe に用いることができないということです。これは型の理論上非常に重要なことですが、詳しくはまた別の章で扱うことにしましょう。

では最後に不在を表す type をまとめておきましょう

  • null: 値の不在を意味する
  • undefined: まだ値がアサインされていない変数
  • void: return statement を持たない関数
  • never: 値を決して return しない関数

Enums

Enum はある type が取りうる value を列挙するために用いられます。Enum は順序がないデータ構造であり、key と value が対応関係にあります。つまり object のようなものであり、ただしコンパイル時にその key が固定され点が異なります。それによって TypeScript は特定の key が本当に存在するのかをチェックすることができます。

enum には二種類あります。一つは key が文字列で値が value も文字列のもの。もう一つは、key が文字列で、value が数値のものです。

enum Language {
  English,
  Spanish,
  Russian
}

メモ:慣習として enum の名称は Uppercase の単数形にします。key も同様に Uppercase にします。

TypeScript は自動的に enum のメンバーそれぞれの value として number を割り当てますが、明示的に自分で value を指定することも可能です。でじゃ明示的に値を指定してみましょう。

eunm
enum Language {
  English = 0,
  Spanish = 1,
  Russian = 2
}

enum から value を得るためには、ドット記法もしくはブラケット記法を用いることができます。通常の object から値を得るための手法と同じですね。

get value
let myFirstLanguage = Language.Russian;
// Language
let mySecondLanguage = Language["English"]; 
// Language

enum の宣言を分割することもできます。そうした場合、TypeScript は自動的にそれらをマージします。その場合これらの宣言のうち、一つだけしか TypeScript は推測をしないので、明示的に enum のメンバーに対して値を指定した方がいいでしょう。

分割して enum を宣言する
enum Language {
  English = 0,
  Spanish = 1
}
enum Language {
  Russian = 2
}

値を計算させることもできます。また、全てを定義しなくても一部を推測させることもできます。

computed value, infer value
enum Language {
  English = 100,
  Spanish = 200 + 300,
  Russian // TypeScript infers 501 (the next number after 500) }
}

Enum の値は string もしくは、string と number の組み合わせを用いることができます。

enum
enum Color {
  Red = "#c10000",
  Blue = "#007ac1",
  Pink = 0xc10050, // A hexadecimal literal
  White = 255 // A decimal literal
}
let red = Color.Red; // Color
let pink = Color.Pink; // Color

Enum にアクセする方法は、value でも key いいのですが、しかしそうすると容易に unsafe な状態に陥ります。

access
let a = Color.Red; // Color
let b = Color.Green;
// Error TS2339: Property 'Green' does not exist
// on type 'typeof Color'.
let c = Color[0]; // string
let d = Color[6]; // string (!!!)

Color[6] は値を取得することができないはずですが、実際には TypeScript はそれを止めてくれないのです!このような unsafe なアクセスを許可しないためには、より安全な enum の subset を const enum で指定する必要があります。では先ほどの enum をこれを使って書き直してみましょう。

more safe enums
const enum Language {
  English,
  Spanish,
  Russian
}

// Accessing a valid enum key
let a = Language.English; // Language

// Accessing an invalid enum key
let b = Language.Tagalog;
// Error TS2339: Property 'Tagalog' does not exist
// on type 'typeof Language'.

// Accessing a valid enum value
let c = Language[0];
// Error TS2476: A const enum member can only be
// accessed using a string literal.

// Accessing an invalid enum value
let d = Language[6];
// Error TS2476: A const enum member can only be
// accessed using a string literal.

const enum はリバースルックアップをしないので、普通の JavaScript における object のような挙動をします。またデフォルトではこの際、余計な JavaScpt コードを生成せず、単に enum member の値が使われる場所では、その値がインラインで記されるだけです。(例えば Language.Spanish が出てくる場所には全て単に 1 という値に置き換えられている)

では const enum をどう用いるのかみてみましょう。

enum
const enum Flippable {
  Burger,
  Chair,
  Cup,
  Skateboard,
  Table
}
function flip(f: Flippable) {
  return "flipped it";
}
flip(Flippable.Chair); // 'flipped it'
flip(Flippable.Cup); // 'flipped it'
flip(12); // 'flipped it' (!!!)

良さそうですね。Chairs と Cups は期待している通りに動いています。しかし、なんと…全ての number が enum しか受け取らない flip にアサインできてしまっています!残念ながらこれは TypeScript の assignability rules の帰結であって、こうさせないためにはさらなる配慮ある行為を行わねばなりません。つまり enum の値は必ず string にするのです。

enum
const enum Flippable {
  Burger = "Burger",
  Chair = "Chair",
  Cup = "Cup",
  Skateboard = "Skateboard",
  Table = "Table"
}
function flip(f: Flippable) {
  return "flipped it";
}
flip(Flippable.Chair); // 'flipped it'
flip(Flippable.Cup); // 'flipped it'
flip(12);
// Error TS2345: Argument of type '12' is not
// assignable to parameter of type 'Flippable'.
flip("Hat");
// Error TS2345: Argument of type '"Hat"' is not
// assignable to parameter of type 'Flippable'.

厄介な value が number である enum が存在した瞬間に enum の安全性は崩壊してしまうのです。

忠告:Enum を安全に使おうと思っても、いくつもの落とし穴があるので、enum を使わないことを推奨する。TypeScript には同様の目的を達成する他の方法がたくさんある。また、それでも同僚が enum を使うことに固執し、考えを変えないようであれば、忍者のごとく穏便に TSLint に変更を加えてしまおう。numeric value にさせない、それから const enum 以外の enum を使わせないルールに。

まとめ

端的に言って TypeScript には最初からかなりの type をサポートしている。TypeScript に type を value から推測させることも、あなたが明示的に type を指定することもできる。const を使うことでより特定の type を推測させることができ、let や var を使った場合にはそれよりも広い範囲の type が推測されることとなる。ほとんどの type には、一般化された type とさらに絞り込んだ type があり、それらには対応関係がある。後者は、前者の subtype となる。

  • general type: subtype
  • boolean: boolean literal
  • bigint: biging literal
  • string: string literal
  • symbol: unique symbol
  • object: object literal
  • array: tuple
  • enum: const enum

練習問題

1

それぞれの値について、TypeScript はどのように type を推測するか

exersize1
let a = 1042;
let b = "apples and oranges";
const c = "pineapples";
let d = [true, true, false];
let e = { type: "ficus" };
let f = [1, false];
const g = [3];
let h = null; //(実際に試してみて、その結果が以外であれば type widing の項目を見よ)

2

それぞれのケースにおいて、なぜ error が throw されるのか述べよ

exersize2
// a
let i: 3 = 3;
i = 4; // Error TS2322: Type '4' is not assignable to type '3'.

// b
let j = [1, 2, 3];
j.push(4);
j.push("5");
// Error TS2345: Argument of type '"5"' is not
// assignable to parameter of type 'number'.

// c
let k: never = 4;
// Error TSTS2322: Type '4' is not assignable
// to type 'never'.

// d
let l: unknown = 4;
let m = l * 2;
// Error TS2571: Object is of type 'unknown'.