本の内容はとても分かりやすく実践に役立つのでオススメです!
この本で学んだことや、大事だなぁと思ったことを書くので、内容とは一致しない部分もあります。
TypeScriptを学び始めたときに読めていたらもっと良いコードが書けていたと思う
第一章
TypeScript と JavaScript の違い
私は TypeScript を勉強し始めたとき、JavaScript との違いを区別できませんでした。
でも、端的に説明すると次の言葉に集約されます。
TypeScriptプログラミングをする際にTypeScript固有の部分は型の部分だけで、ほかの部分はJavaScriptと同じです
例1:変数の型の部分 : string
を削除すると JavaScript になる
// TypeScript
let name: string;
// JavaScript
let name;
例2:関数の引数や返り値にも型がつく。それを削除すると JavaScript になる
// TypeScript
function isAdult(age: number): boolean {
return age >= 18;
}
// JavaScript
function isAdult(age) {
return age >= 18;
}
例3:TypeScriptの型宣言(type) はまるっと削除したらJavaScriptになる
// TypeScript
type Human = {
name: string;
age: number;
}
const human: Human = {
name: 'Taro',
age: 18
}
// JavaScrip
const human = {
name: 'Taro',
age: 18
}
逆に言うと、JavaScript にどうやって型を書けばいいか分かれば、基本的な TypeScript を書くことができます。(極端すぎるけど、分かりやすく言うとそういうこと)
TypeScript は JavaScript に変換してから使う
TypeScript のファイルは拡張子が .ts
になってますが、これをどうやって Web ブラウザやNode.js が使えるようになるのでしょうか? Web ブラウザは、 html, css, js ファイルを解釈できますが ts は解釈できません。
実は、 TypeScript コンパイラが .ts
ファイルを .js
ファイルに変換(トランスパイル)することで初めて使えるようになります。
注意点として、TypeScript コンパイラ(tsc)は ES2015 など指定した ECMAScript の構文に変換はしてくれますが、あくまで構文だけの変換で、使用している関数はそのままです。そのため、IE11対応が必要な場合などは、Babel などで更に IE11 が解釈できる JavaScript にトランスパイルする必要があります。
第二章
変数宣言はできるだけ let より const を使う
const は再代入ができない変数宣言なので、宣言時の代入された値を知っていればその後の処理が読みやすくなります。
let は再代入可能な変数宣言なので、「処理の中で変数の値が変わることがありますよ」と暗に伝えており、読む人の負担が増えます。
例えば、下の例では、最初に宣言された変数 isSuccess が let で宣言されているため、その後の処理を読む場合に、「いつか isSuccess の値が変わることもあるのでは?」ということを考えながら読む必要があります。
例:悪い例
let isSuccess = getHogeHoge();
// 何か処理...
if (isSuccess) {
// 成功処理
} else {
// 失敗処理
}
第三章
型宣言は interface より type を使っておけばOK
C# 育ちのプログラマー(私)にとっては「クラスの型宣言するなら interface = 型といえば interface」と考えていましたが、TypeScript(JavaScript) ではクラスを使うことは大抵オブジェクトでできるので、そもそもクラスの登場機会が多くありません。
そして、昔の TypeScript と違って今の type 宣言は interface 宣言とほぼ同じことができます。
ただ、注釈に書かれているように、 Declaration Merging
を使いたい場合は interface でしかできないようです。
例:Declaration Merging
型宣言を複数書くとマージされる
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = { height: 5, width: 6, scale: 10 };
型をつけると理解しやすくなる
良い型を付けることで読み手に内容を伝えることができます。
例えば、下記の 記事を表した Post という型の場合、description の扱い方が改良版では中身に html の文字列が入っていることが型だけで判断できます。
例:普通の宣言
type Post = {
title: string;
description: string;
}
例:改良版
type HtmlString = string;
type Post = {
title: string;
description: HtmlString;
}
タプル型は便利
2つ以上の異なる型を作れます。例えば、Promise.all の返り値はタプル型です。これを受け取るときは分割代入を使うとスッキリ書けます。
例:Promise.all の返り値はタプル型
const getName = Promise.resolve('Taro');
const getAge = Promise.resolve(18);
Promise.all([getName, getAge]).then((values) => {
// values: [string, number]
// values[0]: "Taro"
// values[1]: 18
})
// 分割代入で受け取るとそのまま変数に代入できて良い
Promise.all([getName, getAge]).then(([name, age]) => {
// name: "Taro"
// age: 18
})
他にも、関数の戻り値でも活用できます。
例:結果とエラー内容を返り値にする関数
function hoge(): [result: number, errors: string[]] {
// 何か処理
return [100, ['message1', 'message2']]
}
const [result, errors] = hoge();
第四章
関数の巻き上げ機能
関数宣言には巻き上げ機能があるけど、関数式には無いということを理解しておく。
例:関数式の普通の書き方
const hoge = () => {
return 'hoge'
}
const value = hoge();
例:関数式のエラーになる書き方
// 関数式 hoge の呼び出しを宣言の前に持ってきたらエラーになる
const value = hoge();
// > Block-scoped variable 'hoge' used before its declaration.(2448)
// > Variable 'hoge' is used before being assigned.(2454)
const hoge = () => {
return 'hoge'
}
一方、関数宣言では関数の呼び出しの順番を問わず呼び出せます。これを関数の巻き上げと言います。
例:関数宣言の巻き上げ
const value = hoge();
// 関数式を関数宣言に変更
function hoge() {
return 'hoge'
}
これだけ見ると巻き上げのある関数宣言を使ったほうが良いと思うけど、それ以上に this の扱いやすさから関数式を使っていきたいと思います。(第五章参照)
高階関数を使うと共通処理をまとめることができる
当たり前のことを言うようですが、関数の引数には String や Number などのプリミティブな値や、 配列や連想配列のようなオブジェクトを渡せます。
TypeScript(JavaScript)では関数もオブジェクトに含まれるので、関数の引数に関数(これをコールバック関数と言う)を渡すことができます。
例:
// これが高階関数(コールバック関数を引数に受け取る関数)
const move = (cb: () => void) => {
console.log('前処理');
cb();
console.log('後処理');
}
const up = () => move(() => console.log('上に移動'));
const down = () => move(() => console.log('下に移動'));
// 実行してみると
up();
// 前処理
// 上に移動
// 後処理
down();
// 前処理
// 下に移動
// 後処理
積極的に readonly を使って可読性を上げる
readonly
で型宣言することで読み取り専用になります。let ではなく const を使う理由と同じで、コードを読む人の負担が減ります。下記の例から分かるように、 readonly
のついた引数は、この関数の中で値が変更されないことを保証してくれるのです。
例:
const eat = (foods: readonly string[]): void => {
foods.push('アップルパイ'); // readonly をつけることで警告が出る
// > Property 'push' does not exist on type 'readonly string[]'.ts(2339)
foods.forEach((food) => {
console.log(food);
})
}
eat(['ハンバーガー', 'ポテト', 'オレンジジュース']);
// ハンバーガー
// ポテト
// オレンジジュース
TypeScript(JavaScript)では const で宣言したオブジェクト型でも中身を変更できてしまうので、readonly をつけることで不具合の発生も抑制できます。
ただし、個人的にはオブジェクト変数の宣言では型推論が働いてくれるので、わざわざ readonly
を書くのは冗長で面倒な仕様だなと感じます。
例:
// 誤った値の変更は防げるが記述が冗長で可読性が悪い
const foods: readonly string[] = ['ハンバーガー', 'ポテト', 'オレンジジュース'];
// 型推論が効くのでわざわざ readonly を書くのが面倒
//const foods = ['ハンバーガー', 'ポテト', 'オレンジジュース'];
第五章
クラスを安全に実装するためのコツが詰まっている
- override を明示的に書かせるための
noImplicitOverride
コンパイラオプションを有効にする - コンパイラチェックされる private 宣言より、ランタイムでチェッでされる # でプライベート変数の宣言をする
- 継承クラスで値が変更できる
protected
変数は親クラスで制御できないので極力使わない
this の実体を知りたいとき、関数内とアロー関数内のどちらで使われているかを意識する
(this がコードに出てくると ヒョエッ!! となります。)
関数の中の this は呼び出し元のオブジェクトが実体(=this)になるので、関数単体で使うと this が undefined になりランタイムエラーになります。
しかし、アロー関数内の this は外側の関数から引き継がれるのでオブジェクトを生成した時点で決まるため、予測しやすいコードになります。(直感的)
以下、WIP
第六章
- 型と実際の変数の値は違う
- typeof は変数から型を取得する
- keyof はオブジェクト型からプロパティ名の型を取得する
- keyof typeof オブジェクト変数 はよく使う
- ジェネリクス型を使いこなす
- 型アサーションはやめろ
- as const を使って readonly な型のオブジェクト変数にする
第七章
- default export はエディタのサポートが受けられなくなる。変更に弱い
- スクリプトとモジュール の違い
- ESModuleとCommonJSの歴史
第八章
- Promise関数の特徴をしっておくと最適なAPI処理を実装できる
第九章
- よりTypeScriptを厳格につかうための設定
感想
コラムがどれも面白いですし、説明にも思想が漂っていてカッコよかったです。詳細は実際に本を手にとっていただきたいと思います