プログラミングTypeScriptを読んで

enumについて
落とし穴1 数値のenumに数値が割り当てできてしまう
enum Number {
One,
Two,
Three,
}
function printNumber(num: Number) {
console.log(num);
}
printNumber(Number.One);
printNumber(1); // これはコンパイルエラーにならない
落とし穴2 数値enumの場合不正な数値でアクセスできてしまう
enum Number {
One,
Two,
Three,
}
Number.One
Number["One"]
Number[0]
Number[1000] // これはコンパイルエラーにならない
- TSのenumは文字列enumと数値enumの2種類がある
- 数値enumはドットによるアクセスも文字列でプロパティ名を指定してもアクセスできるし、インデックスの数値アクセスもできる
- しかし、定義していないインデックスを指定してもコンパイルエラーにはならない
- さらに、数値enumで型注釈された変数には普通に数値が代入できてしまう
- 文字列enumであればインデックスによるアクセスができないのでいくらか安全だが、値による逆アクセスはできない
enum Number {
One = 'one',
Two = 'two',
Three = 'three',
}
Number.One
Number['One']
Number['one'] // これはできない
以上の理由などからenumはしばしば安全でない。落とし穴2に関してはconst enum
を使うことで回避することはできるが落とし穴1は数値enumを使う限り回避することはできない。
enumの使用は控えるべきという風潮が多いが、もし使うならconst enum
もしくは文字列enumをせめて使うようにしたい。
ちなみにconst enum
を使うとJSのコードを生成せず、数値をインライン展開する。外部公開しないアプリケーション内での使用あれば問題ないが、ライブラリなどでenumを公開する場合、const enum
を提供してしまうと変更があったときに不整合が発生してしまう可能性があるため気をつける必要がある。

関数オーバーロード
以下の記事を読めば十分

classの話
XなどでしばしばTSでclass使うのやめとけと言われていると思うが、いくら考えてもその理由が腑に落ちない。classを使わなくともTSという型の仕組みとその柔軟性から特に不便はないし、関数がトップレベルで宣言できるなど、関数型プログラミングのスタイルに近いとかもわかる。
classを使いたいというのはおそらく何かしらのデータ構造やドメインを表現したいときで、振る舞いや状態をカプセル化したいときでこれらはほぼほぼサーバー側のコードを書いているときに思うことだろう。
フロントで上記が当てはまらないとまでは言わないがフロントはユーザー体験を向上することが目的でありUIを構築するのがまあメインの作業なのでサーバーのコードを書く時と比べると同じ土俵ではなく、あまり必要に駆られることはない気がする。
なのでTSでclass使う使わない論争をおそらくサーバーTSの話。フロントは使いたければ使えばいいけど必要なくね?と思っている人がほとんどだと思う、たぶん
サーバー側で他言語と同じような体験を求めるならclassを使う一択だと思っててそれこそNestJSとかを使えばいいと思う。
ただ、TSはその柔軟な型のおかげで関数型プログラミングに近い型レベルでのプログラミングが可能となっている。そうなると関数型寄りのスタイルにしたほうが筋は良さそうに見えるためそれが理想系なのだと思うが、その採用例が少ないのと関数型に寄せるというのはけっこう難しいことだと思う。レールがないのが一番の理由。
一休さんの事例を見たがあれはあのスタイルを先導してくれるエンジニアがいないと厳しいというか関数型プログラミングにある程度精通していて実力のあるTSエンジニアがいないと無理だと思う。
正解はないのでとりあえずでできるとは思うけど明確な指針を持ってやらないとずっとふわふわした開発体験になると思う。
話が逸れたがTSでclassを使うなは少なくともフロントエンド主体の人にはけっこう関係なくて他言語でサーバー書いてた人からすると、特にJavaとかだとclassは欲しくなるし別に使ってもいいと思う。理想を求める人はclassを使わない関数ベースのサーバーTSに目がいくと思うのでclass使うなとは言うかもしれない。
少なくともよく聞くプロトタイプ汚染があるからclassを使うなはあまり腑に落ちないのでだれか説明してほしい
この話は長くなるし本の内容から話がそれるのでこのくらいで

宣言のマージ
値とtype, interface
type CustomerID = number;
// interface CustomerID { id: number}
const CustomerID = 1;
これを使うと後述のcompanion objectパターンが実現できる
interface
interface FeatureRegistry {
feature1: never;
feature2: never;
}
interface FeatureRegistry {
feature3: never;
}
function execute(feature: keyof FeatureRegistry) {
switch (feature) {
case "feature1":
console.log("feature1");
break;
case "feature2":
console.log("feature2");
break;
case "feature3":
console.log("feature3");
break;
}
}
後述のRegistoryパターンみたいなことができる
enum
enum Language {
English = "en",
Japanese = "ja",
}
enum Language {
Spanish = "es",
}
type LanguageValue = `${Language}`; // "en" | "ja" | "es"
typeとinterface
interfaceは上述のマージができるので拡張性があるのがtypeとの大きな違い。どっちが良いのかはまだ意見わかれそうだけどtype派のが多そう。大事なのはどっちかに統一されていることだと思う。また、どっかで見た気がするがinterfaceは宣言のマージによる拡張ができるのでサードパーティーのライブラリのinterfaceを使用側で拡張できるというのが利点って聞いた気がするがどうだったかな。少なくともあんまりピンとこない
この記事のことかも
ちょっと理解が間違ってそうだったがライブラリ側の話でバージョンが上がるごとにinterfaceにプロパティや関数を追加していくことができるのでTSの標準ライブラリやReactみたいなライブラリではinterfaceが使われることがよくあるよみたいな話だ。この記事の中で紹介されているResistoryパターン
と呼んでいるやつすごい学びになる。interfaceのマージってそういう風に使うのか
companion objectパターン
typeは同名を宣言できないが型と値はTSコンパイラは区別できるので同名で宣言可能でこれを利用するとcompanion objectをTSで表現できる。
type UserID = number;
const UserID = {
create: (id: number): UserID => id,
}
const userID: UserID = UserID.create(1);
console.log(userID);

変性の話
オブジェクトAをオブジェクトBに割り当てられるのはオブジェクトAのすべてのパラメーターがオブジェクトBの対応するパラメータのサブタイプである必要がある。
type X = {
id?: number
name: string
}
type XX = {
id: number
name: string
}
let x: X = {
name: 'John'
}
let xx: XX = {
id: 1,
name: 'John'
}
x = xx
xx = x // これはコンパイルエラーになる
number | undefined
はnumber
型のスーパータイプなのでオブジェクトXにオブジェクトXXは割り当てられるがその逆はできない。
これは4種類あるうちのひとつである共変性の話だそうだ。
TypeScriptにおいてだいたいは共変であるが例外が1つあり、関数のパラメーターは反変だそうだ。ここ確認するのめんどくさいのでこれだけ覚えておけば良さそう。一つ言えるなら、関数のパラメーターが反変でないとサブタイプにしかない関数を呼び出したりしていると反変でないとその関数呼び出しは失敗するからだそうだ。
残りの不変性と双変性は割愛。
ちなみに、変性を実装時に指定できるプログラミング言語もあり、KotlinとかがそうらしくClaudeに聞いてみた。
List(共変)
// Kotlin標準ライブラリのList定義(簡略化)
interface List<out E> : Collection<E> {
operator fun get(index: Int): E
// fun add(element: E): Boolean // これは不可能(MutableListにのみ存在)
}
fun processAnimals(animals: List<Animal>) {
animals.forEach { println(it.name) }
}
fun main() {
val dogs: List<Dog> = listOf(Dog("Buddy", "Golden"), Dog("Rex", "Labrador"))
processAnimals(dogs) // OK - List<Dog> は List<Animal> として使用可能
}
MutableList(反変)
// MutableListは不変(inもoutも指定されていない)
interface MutableList<E> : List<E>, MutableCollection<E> {
operator fun set(index: Int, element: E): E
fun add(element: E): Boolean
}
fun processAnimals(animals: MutableList<Animal>) {
animals.add(Cat("New Cat")) // Animalならなんでも追加可能
}
fun main() {
val dogs: MutableList<Dog> = mutableListOf(Dog("Buddy", "Golden"))
// processAnimals(dogs) // エラー!MutableList<Dog> は MutableList<Animal> ではない
}
in/outって変性の話だったのか、今理解した。

JSの非同期処理とイベントループ
TODOで以下の記事とか読む
コールスタックとWeb APIとスタックキューとイベントループの話なら以下の記事が十分わかりやすかった
Web Workerなどの型安全でない処理に型付するテクニックとかは勉強になった

以下はあんまり深掘りしなくていいかなになったやつ
- namespace
- 動的import
- AMD
- アンビエント型宣言
- ソースマップ
- トリプルスラッシュディレクティブ