プログラミング TypeScriptの読書メモ

TypeScriptの入門書。TypeScriptの文法をそんなにちゃんと勉強したことがなかったので、学びが多くあった。

TypeScriptは、JavaScriptに後から型を追加したため、他の言語には多くない柔軟な型表現を持っていて、それはメリットにもなる反面でTypeScriptらしい書き方を学ぶコストが高くなってしまっている気がする。

多くの多くの静的型付け言語では、意識しなくても型安全になるけど、TypeScriptではそうではない部分が多くある。

一方で、多くの言語でいい感じに書けない処理をTypeScriptは型安全に抽象的に記述できるのが強み。

習うより慣れろ的側面が多いと感じてしまうので、他にもうちょっと実践的なコードを写経したりする必要はありそう。

個人的には明かに便利やんという文法と、何でこんな挙動すんねんという挙動が入り混じっていてうーんという感じ。

型について

anyとunknown

any

anyを使うと型が不明なまま任意の操作を許可してしまう。実質pureなJavaScriptと同じ挙動をすることになるので避けるべき。

unknown

型チェックを行うまで操作ができない。anyよりも安全。止むに止まれぬ理由がある場合はanyよりunknownを使う。

const

constを使うとリテラル型が定義される。特定の値しか入らない型。

object

typescriptのobject型はかなり特殊。フィールド定義までが型になる。

let a = { x : "xxx" } // { x : string} という型になる

型推論後にfieldを足したり減らすような操作はerrorになる。

空object {} の扱い

{}とObjectはガバガバなのでなるべく使わない。objectもできれば使わない。

typescriptbook.jp

stringとStringの違い

stringはプリミティブ型、Stringはinterface。

合併型と交差型

objectに適用した時にちょっとややこしい。A|BではAまたはBのプロパティを備えているobject。

A&Bでは、AかつBとなるようなプロパティを備えているobject。

type A = {a: string}
type B = {b: number}

let d : A | B = {a: "a"} // OK
d = {b: 1} // OK
d = {a: "a", b: 1} // OK
d = {a: "a", b: 1, c: 1} // NG

let c : A & B = {a: "a", b: 1} // OK
c = {a: "a", b: 1, c: 1} // NG

配列

空array [] の扱いがめちゃくちゃ特殊。定義されたスコープではなんでもpushできるけど、そこを離れると型推論が終わり、任意の型を追加することができなくなる。

タプル

固定長の配列を定義し、それぞれのインデックスの型を宣言可能。可変調の要素を定義することも可能。

let x: [number, string] = [1, "a"]

let y: [number, number?, number?] = [1, 1, 1]

let z: [number, number?, ...number[]] = [1, 1]

nullとundefined、void、never

nullとundefinedはほぼ違いがない。慣例的には、undefinedは未定義。nullは欠損。

voidは戻り値を返さない関数の戻り値。neverは決してreturnすることのない関数の戻り値。

enum

enumは値でもキーでもどちらでもアクセス可能。しかしキーでアクセスする場合は安全ではない挙動があるので、const enumを利用した方が良い。

ただし、外部ライブラリに公開する相対にconst enumを使うと、問題が起こる可能性があるので、その場合は通常のenumを使う。

またenumの中に数値があるとenumが安全ではなくなるのでstringで定義するように注意する。そもそも使わない方がいいのでは・・・・?

関数

関数の中でのthis

JavaScriptでは、classのmethodとして定義されていない関数にもthisが存在する。このthisは脆弱性を持っているので、methodの場合以外はなるべく使わない。

function date(this: Date) {
    return this
}

date.call(new Date) // OK

date() // NG

date(new Date) // NG

↑のようにthisの型を指定することができる。この場合、thisがDate型で定義されていない場合、呼び出しが失敗する。

ジェネレーター関数

developer.mozilla.org

イテレータ

全然知らん話題

オーバーロード

type Reservation = string

type Reserve = {
    (from: Date, to: Date, destination: string): Reservation
    (from: Date, destination: string): Reservation
}

let reserve: Reserve = (
    from: Date, 
    toOrDestination: Date | string, 
    destination?: string,
) => {
    return ""
}

型が合体するのすごい。

ジェネリック

Tをつける場所によって動き方がちょっと違う。まあ使う時に読めば良さそう。

クラスとインターフェース

アクセス修飾子

public : どこからでもアクセス可能

protected : このクラスとサブクラスのインスタンスからアクセス可能

private : このクラスのインスタンスからのみアクセス可能

static : Classに紐づくメソッド、インスタンスを作らなくても呼び出せる

抽象クラス

直接のインスタンス化ができない。また、abstractが指定されたメソッドは、抽象クラスを継承したクラスでは実装が必須。

super

小クラスが親クラスのメソッドを呼び出す時に使う。

型としてのthis

抽象クラスを継承したクラスにthisの型が必要なclassを実装している場合、overrideを避けるために、thisを型として利用可能にする。

エイリアスとインターフェースの違い

(1) インターフェースはプリミティブ型などの割り当てができない (2) インターフェースの拡張は型エイリアスの拡張より厳しく割り当て可能性をチェックする (3) インターフェースの宣言はマージされる

クラスは構造的に型づけされる

構造が同じだと別の名前のクラスでも代入可能・・・・。他の言語だと構造が同じでも名前が違うtypeとしていれば区別されるが、typescriptは構造が同じなら同じ型と認識されてしまう。

mixin

ちょっとトリッキーだが、覚えておくべきコーディングパターン。

デコレーター

mixinを実行時ではなくbuild時に適用するための機能。現時点では例外的な機能で、クラス拡張は可能だが、typescriptの型システムがpropertyの追加などを認識しない・・・。(外部のjsのライブラリがpropertyを使う分には問題ない?)

finalクラスをシミュレートする

constructorにprivateをつける -> 他の関数がextends出来なくなる。

ただし、newでの初期化も出来なくなるので、別の初期化関数(createなど)を定義する必要がある

高度な型

型の間の関係

サブタイプとスーパータイプ

BがAのサブタイプである => Aが要求されるところではどこでも、Bを安全に使うことができる

スーパータイプは、サブタイプの逆。

変性

配列、オブジェクト、関数のサブタイプの判定は難しい。個別ルールなので、気になった時に読み返す必要がありそう。

割り当て可能性

anyは何にでも割り当て可能なので注意

型の拡大

TypeScriptの型推論は、ゆるい推論をしがちなので、より推論の幅を狭めたい場合は、明示的に指定したりconstをつける。

constアサーション

constをつけると、すべてのpropertyに再起的に型の拡大を抑えた形でreadonlyを付与する

過剰プロパティチェック

過剰なプロパティが存在するとエラーになるようなチェックが働く時がある(働かない時もある)

型の絞り込み

合併型のタグによる絞り込みだけ要チェック。

高度なオブジェクト型

ルックアップ型

明かに便利。

keyof演算子

genericsと組み合わせると便利。

Record型 & マップ型

mapに型付けをすることができる。マップ型の方がより柔軟なことが実現できる。

組み込みのマップ型として、Record、Partial、Required、Readonly、Pickなどがある。

コンパニオンオブジェクトパターン

同じ名前を共有するオブジェクトとクラスを定義することができる。

関数にまつわる高度な型

この章はほとんどテクニック。

  • タプルに対する柔軟な型推論の仕方
  • ユーザー定義型ガイド
  • 条件型
    • 分配条件
    • inferキーワード (難しいので飛ばした。多分一生使わん)
    • 組み込みの条件型

エスケープパッチ

非nullアサーションはたまにつかう。(!)で、not nullなことを知らせる。定義時に!をつけることで、明確な割り当てアサーションもできる。

名前的型をシミュレートする

めちゃくちゃ大事なこと言っている。が、大規模すぎるapplication以外ではいらないかも。

プロトタイプを安全に拡張する

interfaceを使うと、prototypeなどの組み込みの機能も安全に拡張できる。

エラー処理

  • nullを返す
  • 例外をスローする
  • 例外を返す (合併型を使う)
  • Option型

Option型は特に大変そう・・・。(使う時にちゃんと読めば良さそう)

非同期プログラミングと並行、並列処理

JavaScriptのイベントループ

  • メインスレッドからネイティブの非同期APIを呼び出す
  • 非同期APIを呼び出すとすでに制御がメインスレッドに戻る
  • 非同期APIの操作が完了すると、イベントキューにタスクを格納する
  • メインスレッドは、コールスタックが空になると、イベントキューから保留中のタスクを探して実行する

つまり、以下のsetTimeoutは一生実行されない

setTimeout(() => console.info("a"), 1)
setTimeout(() => console.info("b"), 2)
console.info("c")
while(true) {
}

コールバックの処理

JavaScriptの非同期処理は、コールバックを使って表現されるが、型定義から非同期処理であることがわからない。また複数のコールバックを組み合わせ用とすると記述が複雑化してしまうという問題点がある。

プロミス

プロミスのステートマシンは要確認。

型安全なマルチスレッディング

これまでは1つのCPUスレッド上で実行できる非同期プログラムを検討していたが、CPU負荷の高い処理を行うには並列処理が欲しい。

Web Worker (ブラウザー)

JavaScriptでは、workerという機能を使ってマルチスレッドなプログラミングができる。これ自体は型安全ではないが、TypeScriptを使って型安全な形でmessage passingをラップすることはできる。(こういう実装をしたいときに読み返せば十分そう)