No.
2022-03-09
  • Jan
  • Feb
  • Mar
  • Apr
  • May
  • Jun
  • Jul
  • Aug
  • Sep
  • Oct
  • Nov
  • Dec
  • Sun
  • Mon
  • Tue
  • Wed
  • Thu
  • Fri
  • Sat
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

やったこと

サバイバルTypescriptを読む!

シンボル型 (symbol type)

JavaScriptのシンボル型(symbol type)は、プリミティブ型の一種で、その値が一意になる値です。論理型や数値型は値が同じであれば、等価比較がtrueになります。一方、シンボルはシンボル名が同じであっても、初期化した場所が違うとfalseになります。

const s1 = Symbol("foo");
const s2 = Symbol("foo");
console.log(s1 === s1);
true
console.log(s1 === s2);
false

シンボルの用途

JavaScriptにシンボルが導入された動機は、JavaScriptの組み込みAPIの下位互換性を壊さずに新たなAPIを追加することでした。要するに、JavaScript本体をアップデートしやすくするために導入されたものです。したがって、アプリケーションを開発する場合に限っては、シンボルを駆使してコードを書く機会はそう多くはありません。

bigint型

JavaScriptのbigint型は、数値型よりも大きな整数を扱えるプリミティブ型です。

JavaScriptのbigint型のリテラルは整数値の末尾にnをつけて書きます。

const x = 100n;

bigintリテラルをTypeScriptで用いるには、コンパイラーオプションのtargetをes2020以上にする必要があります。

ボックス化 (boxing)

多くの言語では、プリミティブは一般的にフィールドやメソッドを持ちません。プリミティブをオブジェクトのように扱うには、プリミティブをオブジェクトに変換する必要があります。プリミティブからオブジェクトへの変換をボックス化(boxing)と言います。

// プリミティブ型
const str = "abc";
// ラッパーオブジェクトに入れる
const strObject = new String(str);
// オブジェクトのように扱う
strObject.length; // フィールドの参照
strObject.toUpperCase(); // メソッド呼び出し

自動ボックス化

JavaScriptでは、プリミティブ型の値でもフィールドを参照できたり、メソッドが呼び出せます。

const str = "abc";
// オブジェクトのように扱う
str.length; // フィールドの参照
str.toUpperCase(); // メソッド呼び出し

プリミティブ型の値はオブジェクトではないため、このような操作ができるのは変です。ボックス化する必要があるように思えます。しかし、このようなことができるのは、JavaScriptが内部的にプリミティブ型の値をオブジェクトに変換しているからです。この暗黙の変換を自動ボックス化(auto-boxing)と呼びます。

リテラル型 (literal type)

TypeScriptではプリミティブ型の特定の値だけを代入可能にする型を表現できます。そのような型をリテラル型と呼びます。

リテラル型として表現できるもの リテラル型として表現できるプリミティブ型は次のとおりです。

論理型のtrueとfalse 数値型の値 文字列型の文字列

リテラル型の用途

一般的にリテラル型はマジックナンバーやステートの表現に用いられます。その際、ユニオン型と組み合わせることが多いです。

let status: 1 | 2 | 3 = 1;

オブジェクト

プリミティブ以外はすべてオブジェクト

JavaScriptでは、プリミティブ型以外のものはすべてオブジェクト型です。オブジェクト型には、クラスから作ったインスタンスだけでなく、クラスそのものや配列、正規表現もあります。

プリミティブ型は値が同じであれば、同一のものと判定できますが、オブジェクト型はプロパティの値が同じであっても、インスタンスが異なると同一のものとは判定されません。

オブジェクトリテラル (object literal)

JavaScriptの特徴はオブジェクトリテラル{}という記法を用いて、簡単にオブジェクトを生成できる点です。

// 空っぽのオブジェクトを生成
const object = {};
// プロパティを指定しながらオブジェクトを生成
const person = { name: "Bob", age: 25 };

オブジェクトのプロパティ

JavaScriptのオブジェクトは、プロパティの集合体です。プロパティはキーと値の対です。プロパティの値には、1や”string”のようなプリミティブ型や関数、そして、オブジェクトも入れることができます。

オブジェクト型のreadonlyプロパティ (readonly property)

TypeScriptでは、オブジェクトのプロパティを読み取り専用にすることができます。読み取り専用にしたいプロパティにはreadonly修飾子をつけます。読み取り専用のプロパティに値を代入しようとすると、TypeScriptコンパイラーが代入不可の旨を警告するようになります。

let obj: {
  readonly foo: number;
};
obj = { foo: 1 };
obj.foo = 2;
readonlyは再帰的ではない

readonlyは指定したそのプロパティだけが読み取り専用になります。readonlyはそのオブジェクトが入れ子になっている場合、その中のオブジェクトのプロパティまでをreadonlyにはしません。つまり、再帰的なものではありません。

たとえば、fooプロパティがreadonlyで、foo.barプロパティがreadonlyでない場合、fooへの代入はコンパイルエラーになるものの、foo.barへ直接代入するのはコンパイルエラーになりません。

let obj: {
  readonly foo: {
    bar: number;
  };
};
obj = {
  foo: {
    bar: 1,
  },
};
obj.foo = { bar: 2 };
Cannot assign to 'foo' because it is a read-only property.
obj.foo.bar = 2; // コンパイルエラーにはならない

再帰的にプロパティを読み取り専用にしたい場合は、子や孫の各プロパティにreadonlyをつけていく必要があります。

readonlyはコンパイル時のみ

readonlyはTypeScriptの型の世界だけの概念です。つまり、読み取り専用指定を受けたプロパティがチェックを受けるのはコンパイル時だけです。コンパイルされた後のJavaScriptとしては、readonlyがついていたプロパティも代入可能になります。

すべてのプロパティを一括して読み取り専用にする方法

TypeScriptではプロパティを読み取り専用にするには、読み取り専用にしたい各プロパティにひとつひとつreadonly修飾子をつける必要があります。プロパティ数が多くなるとreadonlyをつけていくのは記述量が多くなり手間です。

そういったケースではユーティリティ型のReadonlyを使うのも手です。Readonlyはプロパティをすべて読み取り専用にしてくれる型です。

let obj: Readonly<{
  a: number;
  b: number;
  c: number;
  d: number;
  e: number;
  f: number;
}>;

readonlyとconstの違い

constは変数への代入を禁止にするもの

constは変数への代入を禁止するものです。たとえば、constで宣言されたxに値を代入しようとすると、TypeScriptではコンパイルエラーになり、JavaScriptでは実行時エラーになります。

const x = 1;
x = 2;
Cannot assign to 'x' because it is a constant.

constの代入禁止が効くのは変数そのものへの代入だけです。変数がオブジェクトだった場合、プロパティへの代入は許可されます。

const x = { y: 1 };
x = { y: 2 }; // 変数そのものへの代入は不可
Cannot assign to 'x' because it is a constant.
x.y = 2; // プロパティへの代入は許可
readonlyはプロパティへの代入を禁止にするもの

TypeScriptのreadonlyはプロパティへの代入を禁止するものです。たとえば、readonlyがついたプロパティxに値を代入しようとすると、コンパイルエラーになります。

constは変数自体を代入不可するものです。変数がオブジェクトの場合、プロパティへの代入は許可されます。一方、readonlyはプロパティを代入不可にするものです。変数自体を置き換えるような代入は許可されます。以上の違いがあるため、constとreadonlyを組み合わせると、変数自体とオブジェクトのプロパティの両方を変更不能なオブジェクトを作ることができます。

余剰プロパティチェック (excess property checking)

TypeScriptのオブジェクト型には余剰プロパティチェック(excess property checking)という、追加のチェックが働く場合があります。余剰プロパティチェックとは、オブジェクト型に存在しないプロパティを持つオブジェクトの代入を禁止する検査です。

たとえば、{ x: number }はプロパティxが必須なオブジェクト型です。この型に{ x: 1, y: 2 }のような値を代入しようとします。この代入は許可されるでしょうか。代入値の型は、必須プロパティの{ x: number }を満たしているので問題なさそうです。ところが、この代入は許可されません。

このとき、「Object literal may only specify known properties, and ‘y’ does not exist in type ‘{ x: number; }’.」というコンパイルエラーが発生します。なぜこれがコンパイルエラーになるかというと、{ y: 2 }が余計だと判断されるからです。こうした余計なプロパティを許さないTypeScriptのチェックが余剰プロパティチェックなのです。

インデックス型 (index signature)

TypeScriptで、オブジェクトのフィールド名をあえて指定せず、プロパティのみを指定したい場合があります。そのときに使えるのがこのインデックス型(index signature)です。たとえば、プロパティがすべてnumber型であるオブジェクトは次のように型注釈します。

let obj: {
  [K: string]: number;
};

フィールド名の表現部分が[K: string]です。このKの部分は型変数です。任意の型変数名にできます。Kやkeyにするのが一般的です。stringの部分はフィールド名の型を表します。インデックス型のフィールド名の型はstring、number、symbolのみが指定できます。

インデックス型のオブジェクトであれば、フィールド名が定義されていないプロパティも代入できます。たとえば、インデックス型{ [K: string]: number }には、値がnumber型であれば、aやbなど定義されていないフィールドに代入できます。

let obj: {
  [K: string]: number;
};
obj = { a: 1, b: 2 }; // OK
obj.c = 4; // OK
obj["d"] = 5; // OK
Record<K, T>を用いたインデックス型

インデックス型はRecord<K, T>ユーティリティ型を用いても表現できます。次の2つの型注釈は同じ意味になります。

let obj1: { [K: string]: number };
let obj2: Record<string, number>;

プロトタイプベース

オブジェクトの生成

オブジェクト指向プログラミング(OOP)では、オブジェクトを扱います。オブジェクトを扱う以上は、オブジェクトを生成する必要があります。

しかし、オブジェクトの生成方式は、OOPで統一的な決まりはありません。言語によって異なるのです。言語によりオブジェクト生成の細部は異なりますが、生成方法は大きく分けて「クラスベース」と「プロトタイプベース」があります。

クラスベースとは
JavaやPHP、Ruby、Pythonなどはクラスベースに分類されます。クラスベースでのオブジェクト生成は、オブジェクトの設計図である「クラス」を用います。クラスに対してnew演算子を用いるなどして得られるのがオブジェクトであり、クラスベースの世界では、それを「インスタンス」と呼びます。

プロトタイプベースとは
一方のJavaScriptのオブジェクト生成はプロトタイプベースです。プロトタイプベースの特徴は、クラスのようなものが無いところです。(あったとしてもクラスもオブジェクトの一種だったりと特別扱いされていない)

クラスベースではオブジェクトの素となるものはクラスでした。プロトタイプベースには、クラスがありません。では、何を素にしてオブジェクトを生成するのでしょうか。答えは、「オブジェクトを素にして新しいオブジェクトを生成する」です。

たとえば、JavaScriptでは既存のオブジェクトに対して、Object.create()を実行すると新しいオブジェクトが得られます。

「プロトタイプ」とは日本語では「原型」のことです。プロトタイプベースは単純に言ってしまえば、原型となるオブジェクトを素にオブジェクトを生成するアプローチなのです。

継承

継承についても、クラスベースとプロトタイプベースでは異なる特徴があります。クラスベースでは、継承するときはextendsキーワードなどを用いてクラスからクラスを派生させ、派生クラスからオブジェクトを生成する手順を踏みます。

一方、プロトタイプベースのJavaScriptでは、継承もオブジェクトの生成と同じプロセスで行います。
継承と言ってもプロトタイプベースでは、クラスベースのextendsのような特別な仕掛けがあるわけではなく、「既存のオブジェクトから新しいオブジェクトを作る」というプロトタイプベースの仕組みを継承に応用しているにすぎません。

class構文が使える近年のJavaScript開発では、Object.createを多用したり、無理にプロトタイプベースを意識したコードにする必要もそうそう無いので心配しないでください。ただ、class構文があると言っても、JavaScriptがクラスベースに転向したのではなく、クラスベース風の書き方ができるにすぎません。かくいうclass構文もプロトタイプベースの仕組みの上に成り立っており、JavaScriptのオブジェクトモデルはプロトタイプベースなので、この点は頭の片隅に入れておく必要があります。

なぜJavaScriptはプロトタイプベースなのか?

JavaScriptの開発には次のような要件がありました。ブラウザで動く言語で、構文はJava風に。しかし、Javaほど大掛かりでないようにと。そして、開発期間はというと、10日と逼迫したものでした。

クラスベースの言語を作るのは、プロトタイプベースの言語を作るより難しいと言われています。JavaScriptを作るのに与えられた時間は非常に少ないものでしたから、工数削減にもプロトタイプベースは一役買ったことでしょう。

Javaに似せよと言われて作られたJavaScript。Javaはクラスベースですが、JavaScriptはプロトタイプベースです。では、JavaScriptは泣く泣くクラスベースを諦めたのでしょうか。実はそうではありません。

JavaScriptはクラスベースにするつもりはハナからなかったわけです。Eich氏はJavaScriptを設計するにあたって、できるだけ言語をシンプルにしたいと考えていたようです。JavaScriptはプリミティブ型の種類が少なかったり、プリミティブ型もオブジェクトのようにメソッドが使えるようになっていてプリミティブとオブジェクトの間に大きな隔たりが無かったりします。こうした言語設計もシンプルさを目指したからだそうです。

JavaScriptの開発にあたり、Selfという言語の影響があったとEich氏は言います。Selfは1990年に発表されたプロトタイプベースのオブジェクト指向言語です。Selfの発表論文に掲げられたタイトルは「The Power of Simplicity」つまり「シンプルさの力」です。Selfはクラスを用いたオブジェクト指向プログラミングよりも、プロトタイプベースのほうが言語が単純化されると同時に柔軟になると主張しました。Selfはクラスだけでなく、関数と値の区別や、メソッドとフィールドの区別も撤廃したシンプルさを追求した言語です。言語は単純になると、言語の説明も簡単になり学びやすくもなります。シンプルにするために継承やクラスを諦めたかというとそうではなく、逆に柔軟さが生まれるので、クラスのようなものや継承もプロトタイプを応用すれば実現できるとSelfは主張しています。

JavaScriptがプロトタイプベースを採用したことで、実際に柔軟なプログラミングが行えるようになっています。その一例として、プロトタイプを応用してクラス風のオブジェクト指向を実現するイディオムが生まれ、それがclass構文として言語仕様に取り込まれたり、プロトタイプをプログラマが拡張することで古い実行環境でも最新バージョンのJavaScriptのメソッドが使えるようにするポリフィルが誕生してきました。

object、Object、{}の違い

object型やObject型、{}型の3つは類似する部分がありますが、object型と他の2つは異なる点があります。

object型はオブジェクト型の値だけが代入できる型です。JavaScriptの値はプリミティブ型かオブジェクト型かの2つに大分されるので、object型はプリミティブ型が代入できない型とも言えます。

Object型はインターフェースです。valueOfなどのプロパティを持つ値なら何でも代入できます。したがって、Object型にはnullやundefinedを除くあらゆるプリミティブ型も代入できます。文字列型や数値型などのプリミティブ型は自動ボックス化により、オブジェクトのようにプロパティを持てるからです。

Object型はTypeScriptの公式ドキュメントで使うべきでないとされています。理由はプリミティブ型も代入できてしまうためです。もしオブジェクト型ならなんでも代入可にしたい場合は、代わりにobject型を検討すべきです。

{}型は、プロパティを持たないオブジェクトを表す型です。プロパティを持ちうる値なら何でも代入できます。この点はObject型と似ていて、nullやundefinedを除くあらゆる型を代入できます。

オプショナルチェーン (optional chaining)

JavaScriptのオプショナルチェーン?.は、オブジェクトのプロパティが存在しない場合でも、エラーを起こさずにプロパティを参照できる安全な方法です。

Null合体演算子と組み合わせる オプショナルチェーンがundefinedを返したときに、デフォルト値を代入したい場合があります。その際には、Null合体演算子??を用いると便利です。

const book = undefined;
const title = book?.title ?? "デフォルトタイトル";
console.log(title);
"デフォルトタイトル"

配列

TypeScriptの要素の型

TypeScriptでは、Type[]型の配列から要素を取り出したとき、その値の型はTypeになります。たとえば、string[]型から0番目の要素の型はstringになります。

JavaScriptでは存在しないインデックスで要素アクセスした場合、エラーにならず、代わりにundefinedが得られると説明しましたが、TypeScriptでも不在要素へのアクセスについて、コンパイラーが警告することはありません。

要素アクセスで得た値はstringとundefinedどちらの可能性もありながら、TypeScriptは常にstring型であると考えるようになっています。そのため、要素アクセスでundefinedが返ってくる場合のエラーはTypeScriptでは発見できず、JavaScript実行時に判明することになります。

読み取り専用の配列 (readonly array)

TypeScriptでは配列を読み取り専用(readonly)として型注釈できます。型注釈の方法は2とおりあります。1つ目はreadonlyキーワードを使う方法です。2つ目はReadonlyArray<T>を使う方法です。

読み取り専用配列の特徴

読み取り専用の配列には、配列に対して破壊的操作をするpushメソッドやpopメソッドが、コンパイル時には無いことになります。したがって、readonly number[]型の変数numsに対して、nums.push(4)をするコードはコンパイルエラーになります。

これは、破壊的操作系のメソッドを呼び出そうとするコードがTypeScriptコンパイラーに警告されるだけです。配列オブジェクトからpushメソッドを削除しているわけではありません。なので、JavaScript実行時にはpushメソッドが残っている状態になります。

配列の破壊的操作

JavaScriptの配列メソッドには、破壊的なメソッドと非破壊的なメソッドの2種類があります。特に、破壊的なメソッドは注意深く使う必要があります。

非破壊的なメソッド

非破壊的なメソッドは、操作に配列の変更をともなわないメソッドです。たとえば、concatは非破壊的なメソッドです。これは複数の配列を結合するメソッドです。もとの配列は書き換えず、新しい配列を返します。

破壊的なメソッド

破壊的なメソッドは、配列の内容や配列の要素の順番を変更する操作をともなうメソッドです。たとえば、pushは破壊的メソッドの1つです。これは、配列末尾に要素を追加します。

特に要注意な破壊的なメソッド reverseメソッドは配列を逆順にした配列を返します。戻り値があるので、一見すると非破壊なメソッドに見えなくもありません。しかし、このメソッドは配列の順番も逆にしてしまうので注意が必要です。

破壊的なメソッドを安全に使う方法

破壊的なメソッドを非破壊的に使うには、破壊的操作を行う前に、配列を別の配列にコピーします。配列のコピーはスプレッド構文…を用います。

コピーした配列に対して破壊的操作を行えば、もとの配列が変更される心配が無くなります。

const original = [1, 2, 3];
const copy = [...original]; // コピーを作る
copy.reverse();
console.log(original); // 破壊的操作の影響がない
[ 1, 2, 3 ]
console.log(copy);
[ 3, 2, 1 ]

このreverseの例は、コピーと破壊的なメソッドの呼び出しを1行に短縮して書くこともできます。

const original = [1, 2, 3];
const reversed = [...original].reverse();
console.log(original);
[ 1, 2, 3 ]
console.log(reversed);
[ 3, 2, 1 ]

配列の共変性 (covariance)

型の世界の話で、共変とはその型自身、もしくは、その部分型(subtype)が代入できることを言います。たとえば、Animal型とDog型の2つの型があるとします。DogはAnimalの部分型とします。共変であれば、Animal型の変数にはAnimal自身とその部分型のDogが代入できます。

一方で共変では、Dog型の変数には、DogのスーパータイプであるAnimalは代入できません。

TypeScriptの配列型は共変になっています。たとえば、Animal[]型の配列にDog[]を代入できます。

const dogs: Dog[] = [pochi];
const animals: Animal[] = dogs; // 代入OK

一見するとこの性質は問題なさそうです。ところが、次の例のようにanimals[0]をAnimal型の値に置き換えると問題が起こります。

type Animal = { isAnimal: boolean };
type Dog = {
  isAnimal: boolean;
  wanwan(): string; // メソッド
};

const pochi = {
  isAnimal: true,
  wanwan() {
    return "wanwan"; // メソッドの実装
  },
};

const dogs: Dog[] = [pochi];
const animals: Animal[] = dogs;
animals[0] = { isAnimal: true }; // 同時にdogs[0]も書き換わる
const mayBePochi: Dog = dogs[0];
mayBePochi.wanwan();
// JS実行時エラー: mayBePochi.wanwan is not a function

変数animalsにdogsを代入した場合、animalsの変更はdogsにも影響します。これはJavaScriptの配列がミュータブルなオブジェクトであるためです。animals[0]にAnimal型の値を代入すると、dogs[0]もAnimalの値になります。dogsはDog[]型なので、型どおりならAnimal型を受け付けないことが望ましいですが、実際はそれができてしまいます。その結果、dogs[0]のwanwanメソッドを呼び出すところで、メソッドが存在しないというJavaScript実行時エラーが発生します。

型の安全性を突き詰めると、配列は共変であるべきではないです。型がある他の言語のJavaでは、List<T>型は共変ではなく非変(invariant)になっています。非変な配列では、その型自身しか代入できないようになり、上のような問題が起こらなくなります。

TypeScriptで配列が共変になっている理由

配列が非変である言語がある中、TypeScriptはなぜ型の安全性を犠牲にしてまで配列を共変にしているでしょうか。それはTypeScriptが健全性(soundness)と利便性のバランスを取ること目標にして、型システムを設計しているためです。配列が非変であると健全性は高くなりますが、利便性は下がります。

タプル

TypeScriptの関数は1値のみ返却可能です。戻り値に複数の値を返したい時に、配列に返したいすべての値を入れて返すことがあります。

タプルを使う場面

TypeScriptで非同期プログラミングをする時に、時間のかかる処理を直列ではなく並列で行いたい時があります。そのときTypeScriptではPromise.all()というものを使用します。このときタプルが役に立ちます。 Promiseについての詳しい説明は本書に専門の頁がありますので譲ります。ここではPromise<T>という型の変数はawaitをその前につけるとTが取り出せることだけ覚えておいてください。また、このTをジェネリクスと言いますが、こちらも専門の頁があるので譲ります。

const tuple: [string, number] = await Promise.all([
  takes3Seconds(),
  takes5Seconds(),
]);

このときPromise.all()の戻り値を受けた変数tupleは[string, number]です。実行する関数のPromise<T>のジェネリクスの部分とタプルの型の順番は一致します。つまり次のように入れ替えたら、入れ変えた結果のタプルである[number, string]が得られます。

const tuple: [number, string] = await Promise.all([
  takes5Seconds(),
  takes3Seconds(),
]);

Promise.all()は先に終了した関数から順番に戻り値のタプルとして格納されることはなく、元々の順番を保持します。take3seconds()の方が早く終わるから、先にタプルに格納されるということはなく、引数に渡した順番のとおりにタプルtupleの要素の型は決まります。

列挙型 (enum)

TypeScriptでは、列挙型(enum)を用いると、定数のセットに意味を持たせたコード表現ができます。

列挙型を宣言するには、enumキーワードの後に列挙型名とメンバーを書きます。

enum Position {
  Top,
  Right,
  Bottom,
  Left,
}

enumキーワードはTypeScript独自のものです。なのでJavaScriptにコンパイルすると次のようなコードになります。

var Position;
(function (Position) {
    Position[Position["Top"] = 0] = "Top";
    Position[Position["Right"] = 1] = "Right";
    Position[Position["Bottom"] = 2] = "Bottom";
    Position[Position["Left"] = 3] = "Left";
})(Position || (Position = {}));

数値列挙型 (numeric enum)

TypeScriptの数値列挙型(numeric enum)はもっとも典型的な列挙型です。メンバーの値は上から順に0からの連番になります。

enum Position {
  Top, // 0
  Right, // 1
  Bottom, // 2
  Left, // 3
}

文字列列挙型 (string enum)

TypeScriptの列挙型では、メンバーの値に文字列も使えます。文字列で構成された列挙型は文字列列挙型(string enum)と呼ばれます。

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

列挙型(enum)の問題点と代替手段

  1. 列挙型はTypeScript独自すぎる

TypeScriptは、JavaScriptを拡張した言語です。拡張といっても、むやみに機能を足すのではなく、追加するのは型の世界に限ってです。こういった思想がTypeScriptにはあるため、型に関する部分を除けば、JavaScriptの文法から離れすぎない言語になっています。

JavaScriptの文法からドラスティックに離れたAltJSもあります。その中で、TypeScriptが多くの開発者に支持されているのは、JavaScriptから離れすぎないところに魅力があるからというのもひとつの要因です。

TypeScriptの列挙型に目を向けると、構文もJavaScriptに無いものであるだけでなく、コンパイル後の列挙型はJavaScriptのオブジェクトに変化したりと、型の世界の拡張からはみ出している独自機能になっています。TypeScriptプログラマーの中には、この点が受け入れられない人もいます。

  1. 数値列挙型には型安全上の問題がある

数値列挙型は、number型なら何でも代入できるという型安全上の問題点があります。次の例は、値が0と1のメンバーだけからなる列挙型ですが、実際にはそれ以外の数値を代入できてしまいます。

enum ZeroOrOne {
  Zero = 0,
  One = 1,
}
const zeroOrOne: ZeroOrOne = 9; // コンパイルエラーは起きません!

列挙型には、列挙型オブジェクトに値でアクセスすると、メンバー名を得られる仕様があります。これにも問題があります。メンバーに無い値でアクセスしたら、コンパイルエラーになってほしいところですが、そうなりません。

enum ZeroOrOne {
  Zero = 0,
  One = 1,
}
 
console.log(ZeroOrOne[0]); // これは期待どおり
"Zero"
console.log(ZeroOrOne[9]); // これはコンパイルエラーになってほしいところ…
undefined
  1. 文字列列挙型だけ公称型になる

TypeScriptの型システムは、構造的部分型を採用しています。ところが、文字列列挙型は例外的に公称型になります。

enum StringEnum {
  Foo = "foo",
}
const foo1: StringEnum = StringEnum.Foo; // コンパイル通る
const foo2: StringEnum = "foo"; // コンパイルエラーになる

この仕様は意外さがある部分です。加えて、数値列挙型は公称型にならないので、不揃いなところでもあります。

列挙型の代替案

列挙型の代替案1: ユニオン型

もっともシンプルな代替案はユニオン型を用いる方法です。

type YesNo = "yes" | "no";
function toJapanese(yesno: YesNo) {
  switch (yesno) {
    case "yes":
      return "はい";
    case "no":
      return "いいえ";
  }
}
列挙型の代替案2: オブジェクトリテラル
const Position = {
  Top: 0,
  Right: 1,
  Bottom: 2,
  Left: 3,
} as const;
type Position = typeof Position[keyof typeof Position];
// 上は type Position = 0 | 1 | 2 | 3 と同じ意味になります
function toJapanese(position: Position) {
  switch (position) {
    case Position.Top:
      return "";
    case Position.Right:
      return "";
    case Position.Bottom:
      return "";
    case Position.Left:
      return "";
  }
}