hayashi-ay / booklist

1 stars 0 forks source link

プログラミング TypeScript #12

Closed hayashi-ay closed 1 year ago

hayashi-ay commented 1 year ago

https://www.oreilly.co.jp/books/9784873119045/

練習問題の答え https://github.com/oreilly-japan/programming-typescript-ja

hayashi-ay commented 1 year ago

1章 イントロダクション

hayashi-ay commented 1 year ago

2章 TypeScript:全体像

2.1 コンパイラー

TypeScriptのコンパイルプロセスは以下のようになる。

  1. TypeScriptソース -> TypeScript AST
  2. ASTが型チェッカーによってチェックされる
  3. TypeScript AST -> JavaScriptソース
  4. JavaScriptソース -> JavaScript AST
  5. AST -> バイトコード
  6. バイトコードがランタイムによって評価される

1から3はTSC(TypeScript Compiler)によって行われる、4から6はJavaScriptランタイムによって行われる。

2.2 型システム

型システムとは、プログラムに型を割り当てるために型チェッカーが使用するルールの集まり。型システムには、明示的な構文を使って全ての型をコンパイラーに伝える必要があるもの、型を自動的に推論するものの2つがある。JavaScript、Python、Rubyは実行時に型を推論する、HaskellとOCamlは欠けている型をコンパイル時に推論及びチェックする。ScalaとTypeScriptはいくつかの明示的な型を要求し、残りをコンパイル時に推論及びチェックする。JavaとCはほぼ全てのものについて明示的なアノテーションを必要とし、それらをコンパイル時にチェックする。

TypeScriptは漸進的型付き言語(gradually typed language)である。プログラムのある部分は動的型付け、残りの部分は静的型付けといったことを許容する型システムのこと。

2.3 コードエディタのセットアップ

2.3.1 tsconfig.json

https://www.typescriptlang.org/docs/handbook/tsconfig-json.html

hayashi-ay commented 1 year ago

3章 型について

TypeScriptの型ヒエラルキー。unknownが全ての型のスーパータイプだとすると、neverは全ての型のサブタイプであり、これをボトム型と呼ぶ。

prts_0301

3.1 型についての議論

3.2 型の初歩

3.2.1 any

TypeScriptはコンパイル時にすべてのものが型を持っている必要があるが、型が分からない場合のデフォルトの方がanyとなる。

3.2.2 unknown

unknownはanyと同様に任意の型を表すが、それが何であるかチェックするまでTypeScriptはunknown型の値の使用を許可しない。本当に前もって型がわからない値がある場合にanyの代わりに使用する。

3.2.3 boolean

3.2.4 number

大きな数値を扱うときには、数字の区切りを使って読みやすくすることができる。

let oneMillion = 1_000_000

3.2.5 bigint

number型が表すことのできる整数は2^53までだがbigintはそれよりも大きな整数を表現することができる。

3.2.6 string

3.2.7 symbol

3.2.8 オブジェクト

JavaScriptは構造的に型付けされる。

プリミティブ型についてはconstを使うことで型が狭く推論されるが、オブジェクト型についてはconstを使ってオブジェクト宣言をしても型をより狭く推論するヒントにはならない。なぜならJavaScriptではオブジェクトは変更可能なため。

let airplaneSeatingAssignments: {
    [seatNumber: string]: string
} = {
    '34D': 'Boris Cherny',
    '34E': 'Bill Gates'
}

3.2.9 型エイリアス、合併、交差

type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean}
type CatOrDogOrBoth = Cat | Dog // 合併型
type CatAndDog = Cat & Dog // 交差型

3.2.10 配列

3.2.11 タプル

3.2.12 null、undefined、void、never

3.2.13 列挙型

3.4 練習問題

1. 次のそれぞれの値について、TypeScriptはどのような型推論をするでしょうか?

// number
let a = 1042

// string
let b = 'apples and oranges'

// pineapples
const c = 'pineapples'

// boolean[]
let d = [true, false, true]

// {type: string}
let e = {type: 'ficus'}

// (number | boolean)[]
let f = [1, false]

// number[]
const g = [3]

// any
let h = null

2. 次のそれぞれのものはなぜエラーをスローするのでしょうか?

// a
let i: 3 = 3
i = 4  // iは明示的にリテラル型の3という型で宣言されているのでリテラル型の4を割り当てることはできない

// b
let j = [1, 2, 3]
j.push(4)
j.push('5') // jはnumber[]と推論されるのでstringを追加することはできない

// c
let k: never = 4  // neverはボトム型であり上位型である4を割り当てることはできない

// d 
let l: unknown = 4
let m = l * 2 // unknown型の変数は型が絞り込まれるまで使用することができない
hayashi-ay commented 1 year ago

4章 関数

4.1 関数の宣言と呼び出し

4.1.3 call、apply、bind

4.1.4 thisの型付け

thisは関数の宣言方法によってではなく、関数の呼び出し方によって変わる。 期待するthisの型を巻数の最初のパラメーターとして宣言することでthisの型を検査することができる。 関数シグネチャとして使われる場合はthisは予約語である。

function fancyDate(this: Date) {
    `${this.getMonth() + 1}/${this.getDate()}/${this.getFullYear()}`
}

4.1.5 ジェネレーター

4.1.6 イテレーター

4.1.7 呼び出しシグネチャ

4.1.8 文脈的型付け

4.1.9 オーバーロードされた関数の型

4.2 ポリモーフィズム

4.5 練習問題

1. TypeScriptは、関数シグネチャのうち、どの部分を推論するでしょうか?パラメータでしょうか、戻り値の型でしょうか、それともその両方でしょうか?

基本的にはパラメータについては推論されず戻り値の型のみ推論される。

2. JavaScriptのargumentsオブジェクトは型安全でしょうか?もしそうでないとすると、代わりに何が使えるでしょうか?

可変長引数関数を実現するためにargumentsオブジェクトを使用するのは型安全ではない。引数の型についての推論が行われないのと関数シグネチャとしては可変長の型を受け付けることを宣言できないためである。代わりにレスとパラメータを使うと良い。

3. すぐに出発する旅行を予約する機能が欲しいとします。オーバーロードされたreserve関数を、3番目の呼び出しシグネチャを作成して書き換えてください。このシグネチャは目的地(destination)だけを取り、明示的な出発日(from)は取りません。この新しいオーバーロードされたシグネチャをサポートするように、reserveの実装を書き換えてください。

type Reservation = {};

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

const reserve: Reserve = (
  fromOrDestination: Date | string,
  toOrDestination?: Date | string,
  destination?: string,
) => {
  return {};
};

4. callの実装を、2番目の引数がstringである関数について「だけ」機能するように書き換えてください。そうでない関数を渡すとコンパイル時にエラーとなるようにします。

function call(
    f: (arg: string) => unknown,
    arg: string
): unknown {
    return f(arg)
}

callの第2引数string型だけの場合(コールバック関数が引数を1つのみ持ちその型がstring)と捉えたので模範解答と答えが異なる。設問の意図としては、コールバック関数は複数引数を取るがその第2引数がstring型である場合のみ機能するcall関数を作るということだった。

function call<T extends [unknown, string, ...unknown[]], R>(
    f: (...args: T) => R,
    ...args: T
): R {
    return f(...args)
}

function fill(length: number, value: string): string[] {
    return Array.from({length}, () => value)
}

5. 型安全なアサーション関数、isを実装してください。型で概略を記述することから始めます。これは、完成したら、次のように使えるものです。

is('string', 'other_string')
is(true, false)
is(42, 42)
// is(10, 'foo') // compile error
is(1, 1, 1)
is(1, 1, 3)
// 引数2つ版
function is<T>(
    lhs: T,
    rhs: T
): boolean {
    return lhs == rhs
}

// 引数2つ以上版
function is<T>(
    lhs: T,
    rhs: T,
    ...args: T[]
): boolean {
    return lhs == rhs && args.every(arg => arg === lhs)
}

模範解答

function is<T>(
    a: T,
    ...b: [T, ...T[]]
): boolean {
    return b.every(ele => ele === a);
}
hayashi-ay commented 1 year ago

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

5.1 クラスと継承

5.2 super

5.3 戻り値の型としてthisを使用する

5.4 インターフェイス

5.5 クラスは構造的型付けされる

TypeScriptでは他の型ど同様にクラスについても名前ではなく構造によって型付けされる。

5.6 クラスは値と型の両方を宣言する

TypeScriptでは型と値の名前空間が別々に分かれている。クラス宣言では値レベルと型レベルで語句が生成される。

// 値
const a = 1000

// 型
type a = number

5.7 ポリモーフィズム

5.8 ミックスイン

ミックスインとは多重継承をシミュレーションするための方法である。多重継承の際は菱形継承などを始め問題がいくつかありそれらを解消するための方法の1つである。JavaScriptやTypeScriptでは言語機能としてmixinなどのキーワードは用意されていないが自前で実装することが可能である。

5.9 デコレーター

5.13 練習問題

1. クラスとインターフェイスの違いは何でしょうか?

クラスは実体を持つがインターフェイスは実体を持たない。

模範解答 / クラスは、実装、初期化されたクラスフィールド、アクセス修飾子を持つことができます。また、クラスはJavaScriptコードを生成するので、実行時のinstanceofのチェックもサポートしています。クラスは、型と値の両方を定義します。インターフェースは型だけを定義し、JavaScriptコードはいっさい生成せず、型レベルのメンバーだけを含むことができ、アクセス修飾子を含むことはできません。 /

2. クラスのコンストラクターをprivateと指定すると、そのクラスをインスタンス化したり拡張したりできないという意味になります。代わりにprotectedと指定すると、何が起こるでしょうか? コードエディターでいろいろと試してみてください。

そのクラスもしくは継承したクラスからのみしかインスタンス化することができない。

3. 「5.11.1 ファクトリーパターン」で作成した実装を拡張して、抽象化を多少犠牲にしてでも、より安全にしてください。つまり、Shoe.create('boot')を呼び出すとBootが返され、Shoe.create('balletFlat')を呼び出すとBalletFlatが返されることを(どちらもShoeが返されるのではなく)、利用者がコンパイル時にわかるように、実装を書き換えてください。ヒント:「4.1.9 オーバーロードされた関数の型」を思い出してください。

4. [難問]練習として、型安全なビルダーパターンをどうしたら設計できるか考えてみてください。次のことを実現できるように、「5.11.2 ビルダーパターン」のビルダーパターンを拡張します。

type Shoe = {
    purpose: string
}

class BalletFlat implements Shoe {
    purpose = 'dancing'
}

class Boot implements Shoe {
    purpose = 'woodcutting'
}

class Sneaker implements Shoe {
    purpose = 'walking'
}

type CreateShoe = {
    (type: "balletFlat"): BalletFlat
    (type: "boot"): Boot
    (type: "sneaker"): Sneaker
}

const Shoe: {create: CreateShoe} = {
    create: (type: 'balletFlat' | 'boot' | 'sneaker') => {
        switch(type) {
            case 'balletFlat': return new BalletFlat
            case 'boot': return new Boot
            case 'sneaker': return new Sneaker
        }
    }
}

4a. 少なくともURLとメソッドの設定が終わるまでは.sendを呼び出せないことをコンパイル時に保証します。メソッドを特定の順序で呼び出すことをユーザーに強制したら、これを保証することは容易になるでしょうか?(ヒント:thisの代わりに何を返せるでしょうか?)

返り値の型を以下のようにする。

{
    date: Object,
    method: null,
    url: null
}

模範解答 https://github.com/oreilly-japan/programming-typescript-ja/blob/master/answers/ch05.ts

4b. [より難問]ユーザーがメソッドを任意の順序で呼び出せるようにしたまま、これを保証したいとしたら、設計をどのように変更すればよいでしょうか?

模範解答 https://github.com/oreilly-japan/programming-typescript-ja/blob/master/answers/ch05.ts

hayashi-ay commented 1 year ago

6章 高度な型

6.1 型の間の関係

6.1.2 変性

独自の記法。 「A <: B」、Aが型Bのサブタイプであるか、またはBと同じ型であること 「A >: B」、Aが型Bのスーパータイプであるか、またはBと同じ型であること

TypeScriptでは、複雑な型はすべてそのメンバーに対して共変である。つまりオブジェクトAがオブジェクトBに割り当て可能となるためには、対応するそれぞれのプロパティについて「Aのプロパティ <: Bのプロパティ」でなければいけない。 また一部の言語ではオブジェクトはそのプロパティの型に関して不変である。

関数のパラメータについては例外的に反変である。

6.1.3 割り当て可能性

6.1.4 型の拡大

TypeScriptは型の推論時に寛大であり、考えられる最も具体的な方ではなくより一般的な型を推論する。

6.1.5 型の絞り込み

TypeScriptはフローベースの型推論を行う。Haskell/OCaml/Scalaスタイルのパターンマッチングの代わりになるもの。

6.2 完全性

6.3 高度なオブジェクト型

6.3.1 オブジェクト型についての型演算子

合弁(|)と交差(&)以外にもTypeScriptは型演算子を提供する。

6.3.2 レコード型

6.3.3 マップ型

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

Scalani由来するもので同じ名前を共有するオブジェクトとクラスをペアにする方法。型と値の情報をグループ化することができ、また療法を一度にインポートすることができる。

6.4 関数にまつわる高度な型

6.4.1 タプルについての型推論の改善

6.4.2 ユーザー定義型ガード

6.5 条件型

6.5.1 分配条件型

6.5.2 inferキーワード

6.6 エスケープハッチ

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

6.10 練習問題

1. 次のそれぞれの型のペアについて、最初の型が2番目の型に割り当て可能かどうかを、その理由も添えて答えてください。サブタイプと変性の観点からこれらについて考え、もし確信を持って答えられなければ、章の初めのほうのルールを参照してください(それでも確信が持てなければ、コードエディターに入力してチェックしてください)。

2. type O = {a: {b: {c: string}}} というオブジェクト型がある場合、keyof Oの型は何になるでしょうか? O['a']['b']については、どうでしょうか?

type O = {a: {b: {c: string}}}

type type = keyof O // 'a'
type type2 = O['a']['b'] // {c: string}

3. TかUのどちらかに含まれる(ただし両方には含まれない)型を計算するExclusive<T, U>型を記述してください。たとえば、Exclusive<1 | 2 | 3, 2 | 3 | 4>は、1 | 4になります。Exclusive<1 | 2, 2 | 4>を型チェッカーがどのように評価するかを、ステップごとに書き出してください。

type Exclusive<T, U> = (T extends U ? never : T)| (U extends T ? never : U)
type A = Exclusive<1 | 2 | 3, 2 | 3 | 4> // 1, 4

Exclusive<1 | 2, 2 | 4>の評価の流れ

// start
type A = Exclusive<1 | 2, 2 | 4>

// Exclusiveの定義に置き換えてTとUを適用する
type B = (1 | 2 extends 2 | 4 ? never : 1 | 2) 
        | (2 | 4 extends 1 | 2 ? never : 2 | 4)

// 条件を合弁型全体に分配する
type C = (1 extends 2 | 4 ? never : 1)
        | (2 extends 2 | 4 ? never : 2)
        | (2 extends 1 | 2 ? never : 2)
        | (4 extends 1 | 2 ? never : 4)

組み込み条件型のExcludeを使うのも良い。展開と分配のタイミングがややこしい。本書の例とは展開と分配のタイミングが逆かもしれない。

4. 明確な割り当てアサーションを使わないように、(「6.6.3 明確な割り当てアサーション」の)例を書き直してください。

const globalCache = {
    get(key: string) {
        return `${key}_user_id`
    }
  }

const userId = fetchUser()

userId.toUpperCase()

function fetchUser() {
    return globalCache.get('user_id')
}
hayashi-ay commented 1 year ago

7章 エラー処理

TypeScriptで実行時エラーに対処する方法は以下の4つがある。

7.1 nullを返す

最も軽量だがエラーの詳細についての情報が失われる。

7.2 例外をスローする

軽量でエラーの詳細についての情報を与えることができる。ただし、エンジニアは怠惰なので例外のハンドリングをちゃんとしない恐れがある。

7.3 例外を返す

TypeScriptはJavaのようなthrows節が存在しないが、合弁型を使って同様のことを実現できる。 冗長になるが、安全性が高くなる。

7.4 Option型

Option型はHaskell, OCaml, Scala, Rustなどの言語に由来する。値の代わりにコンテナを返すことであり、コンテナの中には値が入っていたり入っていなかったりする。

言語機能としてはないため、Optionを塩茹しないコードと相互運用することはできない。 パターンマッチがない。

7.6 練習問題

1. この章で紹介したパターンのいずれかを使って、次に示すAPIに関するエラーの処理方法を設計してください。このAPIでは、すべての操作は失敗する可能性があります――失敗を考慮に入れるよう、APIのメソッドのシグネチャを自由に書き換えてください(もしくは、望むのであれば、書き換えずにそのまま使ってください)。発生するエラーを処理しながら、一連のアクションをどのように実行できるかについて考えてください(たとえば、ログインしたユーザーのIDを取得し、彼らの友人のリストを取得し、それぞれの友人の名前を取得する)。

type UserID = string
interface API {
    getLoggedInUserID(): UserID | null
    getFriendId(userID: UserID): UserID[] | null
    getUserName(userID: UserID): string | null
}
hayashi-ay commented 1 year ago

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

8.1 JavaScriptのイベントループ

8.2 コールバックの処理

8.3 プロミスを使って健全さを取り戻す

PromiseとOptionはどちらもHaskellにあるMonadデザインパターンから発想を得ている。

8.4 asyncとawait

8.5 非同期ストリーム

8.5.1 イベントエミッター

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

8.6.1 Web Worker(ブラウザー)

8.6.2 子プロセス(Node.js)

8.8 練習問題

1. 汎用的なpromisify関数を実装してください。promisifyは、1つの引数と1つのコールバックを取る任意の関数をパラメーターとして取り、それを、プロミスを返す関数の中にラップします。

import {readFile} from 'fs'

function promisify<A, T>(f: (arg: A, f: (err: Error | null, data: T) => void) => void)
    : (arg: A) => Promise<T> {
    return (arg: A) => {
        return new Promise<T>((resolve, reject) => {
            f(arg, (error, result) => {
                if (error) {
                    reject(error)
                } else {
                    resolve(result)
                }
            })
        });
    }
}

const readFilePromise = promisify(readFile)
readFilePromise('./myfile.ts')
    .then(result => console.log('success reading file', result.toString()))
    .catch(error => console.error('error reading file', error))

2. 「8.6.1.1 型安全なプロトコル」では、型安全な行列演算のためのプロトコルの半分を作成しました。これをメインスレッドで実行すると仮定して、Web Workerスレッドで実行する残りの半分を実装してください。 https://github.com/oreilly-japan/programming-typescript-ja/blob/master/answers/ch08.ts

3. (「8.6.1 Web Worker(ブラウザー)」のように)マップ型を使って、Node.jsのchild_process用の型安全なメッセージパッシングプロトコルを実装してください。 https://github.com/oreilly-japan/programming-typescript-ja/blob/master/answers/ch08.ts

hayashi-ay commented 1 year ago

9章 フロントエンドとバックエンドのフレームワーク

9.1 フロントエンドのフレームワーク

Reactを使用する場合、 JSX(JavaScript XML)と呼ばれるDSLを使ってビューを定義する。

9.2 型安全なAPI

9.3 バックエンドのフレームワーク

hayashi-ay commented 1 year ago

10章 名前空間とモジュール

10.1 JavaScriptモジュールの簡単な歴史

1995年当初のJavaScriptはモジュールシステムをいっさいサポートしていなかった。そのためすべてのものがグローバルな名前空間の中で宣言されていた。 Node.js(2009)ではCommonJSというモジュールシステムを使用した。 CommonJSではモジュール解決アルゴリズムなどに問題点があり、ES2015でimportやexportをする方法が導入された。

10.2 インポート、エクスポート

10.2.3 モジュールモードとスクリプトモード

10.3 名前空間

10.4 宣言のマージ

10.6 練習問題

1. 宣言のマージをいろいろと試してみて、次のことを行ってください。 a. 値と型の代わりに名前空間とインターフェースを使って、コンパニオンオブジェクトを実装し直してください。 https://github.com/oreilly-japan/programming-typescript-ja/blob/master/answers/ch10.ts

b. 列挙型に静的メソッドを追加してください。 https://github.com/oreilly-japan/programming-typescript-ja/blob/master/answers/ch10.ts

hayashi-ay commented 1 year ago

11章 JavaScriptとの相互運用

11.1 型宣言

11.2 JavaScriptからTypeScriptへの斬新的な移行

hayashi-ay commented 1 year ago

12章 TypeScriptのビルドと実行

12.1 TypeScriptプロジェクトのビルド

12.1.4 ソースマップを有効にする。

ソースマップ(source map)はトランスパイルされたコードを、生成元のソースコードの逆にリンクするための方法。

hayashi-ay commented 1 year ago

13章 終わりに

hayashi-ay commented 1 year ago

付録

hayashi-ay commented 1 year ago

TypeScriptが型をどのように扱うかの理解が深まりました。脚注で他の言語との違いも言及されていて良かったです。6章までが理論的な内容でそれ以降が実践的な内容になっています。単なる翻訳書ではなく原著の執筆時点と翻訳書時点でのTypeScriptのバージョンのアップデートにも対応されていて良かったです。また、付録ではESLintのルールを独自で実装する方法についてASTから説明があって良かったです。各章ごとに練習問題があり理解を深めることができました。