Closed hayashi-ay closed 1 year ago
TypeScriptのコンパイルプロセスは以下のようになる。
1から3はTSC(TypeScript Compiler)によって行われる、4から6はJavaScriptランタイムによって行われる。
型システムとは、プログラムに型を割り当てるために型チェッカーが使用するルールの集まり。型システムには、明示的な構文を使って全ての型をコンパイラーに伝える必要があるもの、型を自動的に推論するものの2つがある。JavaScript、Python、Rubyは実行時に型を推論する、HaskellとOCamlは欠けている型をコンパイル時に推論及びチェックする。ScalaとTypeScriptはいくつかの明示的な型を要求し、残りをコンパイル時に推論及びチェックする。JavaとCはほぼ全てのものについて明示的なアノテーションを必要とし、それらをコンパイル時にチェックする。
TypeScriptは漸進的型付き言語(gradually typed language)である。プログラムのある部分は動的型付け、残りの部分は静的型付けといったことを許容する型システムのこと。
https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
TypeScriptの型ヒエラルキー。unknownが全ての型のスーパータイプだとすると、neverは全ての型のサブタイプであり、これをボトム型と呼ぶ。
TypeScriptはコンパイル時にすべてのものが型を持っている必要があるが、型が分からない場合のデフォルトの方がanyとなる。
unknownはanyと同様に任意の型を表すが、それが何であるかチェックするまでTypeScriptはunknown型の値の使用を許可しない。本当に前もって型がわからない値がある場合にanyの代わりに使用する。
大きな数値を扱うときには、数字の区切りを使って読みやすくすることができる。
let oneMillion = 1_000_000
number型が表すことのできる整数は2^53までだがbigintはそれよりも大きな整数を表現することができる。
JavaScriptは構造的に型付けされる。
プリミティブ型についてはconstを使うことで型が狭く推論されるが、オブジェクト型についてはconstを使ってオブジェクト宣言をしても型をより狭く推論するヒントにはならない。なぜならJavaScriptではオブジェクトは変更可能なため。
[key: T] :U
という構文のこと。インデックスシグネチャを使うと明示的に宣言したキーの他に、より多くのキーを安全にオブジェクトに追加することができる。Tはnumberかstringのどちらかである必要がある。JavaScriptのオブジェクトは、キーとして文字列またはシンボルを使用する。let airplaneSeatingAssignments: {
[seatNumber: string]: string
} = {
'34D': 'Boris Cherny',
'34E': 'Bill Gates'
}
型エイリアス 型に別名をつけることができる。型エイリアスはTypeScriptによって推論されないので明示的に宣言する必要がある。letやconstと同様に型エイリアスはブロックスコープである。
type Age = number
合併型 集合の和に相当する。共用体型、共用型、union型とも訳される。
交差型 集合の積に相当する。
type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean}
type CatOrDogOrBoth = Cat | Dog // 合併型
type CatAndDog = Cat & Dog // 交差型
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型の変数は型が絞り込まれるまで使用することができない
thisは関数の宣言方法によってではなく、関数の呼び出し方によって変わる。 期待するthisの型を巻数の最初のパラメーターとして宣言することでthisの型を検査することができる。 関数シグネチャとして使われる場合はthisは予約語である。
function fancyDate(this: Date) {
`${this.getMonth() + 1}/${this.getDate()}/${this.getFullYear()}`
}
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);
}
TypeScriptでは他の型ど同様にクラスについても名前ではなく構造によって型付けされる。
TypeScriptでは型と値の名前空間が別々に分かれている。クラス宣言では値レベルと型レベルで語句が生成される。
// 値
const a = 1000
// 型
type a = number
ミックスインとは多重継承をシミュレーションするための方法である。多重継承の際は菱形継承などを始め問題がいくつかありそれらを解消するための方法の1つである。JavaScriptやTypeScriptでは言語機能としてmixinなどのキーワードは用意されていないが自前で実装することが可能である。
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
独自の記法。 「A <: B」、Aが型Bのサブタイプであるか、またはBと同じ型であること 「A >: B」、Aが型Bのスーパータイプであるか、またはBと同じ型であること
TypeScriptでは、複雑な型はすべてそのメンバーに対して共変である。つまりオブジェクトAがオブジェクトBに割り当て可能となるためには、対応するそれぞれのプロパティについて「Aのプロパティ <: Bのプロパティ」でなければいけない。 また一部の言語ではオブジェクトはそのプロパティの型に関して不変である。
関数のパラメータについては例外的に反変である。
TypeScriptは型の推論時に寛大であり、考えられる最も具体的な方ではなくより一般的な型を推論する。
TypeScriptはフローベースの型推論を行う。Haskell/OCaml/Scalaスタイルのパターンマッチングの代わりになるもの。
合弁(|
)と交差(&
)以外にもTypeScriptは型演算子を提供する。
Scalani由来するもので同じ名前を共有するオブジェクトとクラスをペアにする方法。型と値の情報をグループ化することができ、また療法を一度にインポートすることができる。
1. 次のそれぞれの型のペアについて、最初の型が2番目の型に割り当て可能かどうかを、その理由も添えて答えてください。サブタイプと変性の観点からこれらについて考え、もし確信を持って答えられなければ、章の初めのほうのルールを参照してください(それでも確信が持てなければ、コードエディターに入力してチェックしてください)。
a. 1とnumber 割り当て可能。1はnumberのサブタイプであるため。
b. numberと1 割り当て不可能。numberは1のスーパータイプであるため。
c. stringとnumber | string 割り当て可能。stringはnumber | stringのサブタイプであるため。
d. booleanとnumber 割り当て不可能。booleanはnumberのスーパータイプでもサブタイプでもない。
e. number[]と(number | string)[] 割り当て可能。配列は共変であり、numberはnumber | stringのサブタイプであるため。
f. (number | string)[]とnumber[] 割り当て不可能。配列は共変であり、number | stringはnumberのスーパータイプであるため。
g. {a: true}と{a: boolean} 割り当て可能。オブジェクトはすべてプロパティについて共変であり、trueはbooleanのサブタイプであるため。
h. {a: {b: [string]}}と{a: {b: [number | string]}} 割り当て可能。オブジェクトはすべてのプロパティについて共変であり、stringはnumber | stringのサブタイプであるため。
i. (a: number) => stringと(b: number) => string 割り当て可能。TypeScriptは構造的型付け言語であるためパラメータのaとbの名前的な違いは関係ない。
j. (a: number) => stringと(b: string) => string 割り当て不可能。関数のパラメータは反変であるが、numberはstringのサブタイプでもスーパータイプでもないため。
k. (a: number | string) => stringと(a: string) => string 割り当て可能。関数のパラメータは反変であり、number | stringはstringのスーパータイプであるため。
l. (列挙型 enum E {X = 'X'}で定義されている)E.Xと(列挙型 enum F {X = 'X'}で定義されている)F.X 模範解答 割り当て可能ではありません。列挙型のメンバーが別の列挙型のメンバーに割り当て可能であるためには、同じ列挙型に由来するものでなければなりません。どちらのメンバーもXという名前であり、同じ文字列値'X'を持っていますが、それらは異なる列挙型について定義されているので、それらを互いに割り当てることはできません。
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')
}
TypeScriptで実行時エラーに対処する方法は以下の4つがある。
最も軽量だがエラーの詳細についての情報が失われる。
軽量でエラーの詳細についての情報を与えることができる。ただし、エンジニアは怠惰なので例外のハンドリングをちゃんとしない恐れがある。
TypeScriptはJavaのようなthrows節が存在しないが、合弁型を使って同様のことを実現できる。 冗長になるが、安全性が高くなる。
Option型はHaskell, OCaml, Scala, Rustなどの言語に由来する。値の代わりにコンテナを返すことであり、コンテナの中には値が入っていたり入っていなかったりする。
言語機能としてはないため、Optionを塩茹しないコードと相互運用することはできない。 パターンマッチがない。
1. この章で紹介したパターンのいずれかを使って、次に示すAPIに関するエラーの処理方法を設計してください。このAPIでは、すべての操作は失敗する可能性があります――失敗を考慮に入れるよう、APIのメソッドのシグネチャを自由に書き換えてください(もしくは、望むのであれば、書き換えずにそのまま使ってください)。発生するエラーを処理しながら、一連のアクションをどのように実行できるかについて考えてください(たとえば、ログインしたユーザーのIDを取得し、彼らの友人のリストを取得し、それぞれの友人の名前を取得する)。
type UserID = string
interface API {
getLoggedInUserID(): UserID | null
getFriendId(userID: UserID): UserID[] | null
getUserName(userID: UserID): string | null
}
PromiseとOptionはどちらもHaskellにあるMonadデザインパターンから発想を得ている。
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
Reactを使用する場合、 JSX(JavaScript XML)と呼ばれるDSLを使ってビューを定義する。
1995年当初のJavaScriptはモジュールシステムをいっさいサポートしていなかった。そのためすべてのものがグローバルな名前空間の中で宣言されていた。 Node.js(2009)ではCommonJSというモジュールシステムを使用した。 CommonJSではモジュール解決アルゴリズムなどに問題点があり、ES2015でimportやexportをする方法が導入された。
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
ソースマップ(source map)はトランスパイルされたコードを、生成元のソースコードの逆にリンクするための方法。
TypeScriptが型をどのように扱うかの理解が深まりました。脚注で他の言語との違いも言及されていて良かったです。6章までが理論的な内容でそれ以降が実践的な内容になっています。単なる翻訳書ではなく原著の執筆時点と翻訳書時点でのTypeScriptのバージョンのアップデートにも対応されていて良かったです。また、付録ではESLintのルールを独自で実装する方法についてASTから説明があって良かったです。各章ごとに練習問題があり理解を深めることができました。
https://www.oreilly.co.jp/books/9784873119045/
練習問題の答え https://github.com/oreilly-japan/programming-typescript-ja