【第16回】第2部の買い物メモを型で守る|TypeScriptへの書き換え(嫁子が教えるTS講座)
今回のゴール
- 動いているJavaScriptをTypeScriptに移す手順が分かる
- strictで出るエラーを、種類ごとに片付けられる
- 型をつけたあと、何が防げるようになったかを説明できる
第3部の最後です。第10回で作った買い物メモを、そのまま型で守ります。全体の流れは講座の目次へ。
嫁子: 今日は新しい文法はほとんど出てこないよ。既にあるコードに型を当てるだけ。
でもこれ、実務でいちばん多い作業なの。ゼロから書き始めることより、すでに動いているものを直す・足すほうが圧倒的に多いからね。
しかも今日は、エラーが14件出るところから始めるよ。全部消すからね。
① 拡張子を変えるところから
まず app.js を app.ts に変えます。それだけで、第15回で入れた strict が全部のチェックを始めます。
npx tsc --noEmit
第10回のコードでは、ここで14件のエラーが出ます。数を見て身構えるかもしれませんが、中身は3種類しかありません。
| 種類 | エラーの言い分 | 件数 |
|---|---|---|
① items の中身が不明 |
「any[] と推論されます」 |
6 |
② 要素が null かもしれない |
「list は null の可能性があります」 |
6 |
③ value を持っていない |
「value は HTMLElement に存在しません」 |
2 |
同じ種類のものは、同じ直し方で片付きます。14件と思うから多く見えるだけで、実際は3回の作業です。
嫁子: エラー一覧を見た瞬間に閉じたくなる気持ち、分かるよ。私も最初はそうだった。
でも種類ごとに数えると、だいたい3〜4種類に収まるの。だからまず分類してから手を動かす。ここだけ覚えておいてね。
② items の中身を宣言する
出ているのはこのエラーです。
error TS7034: Variable 'items' implicitly has type 'any[]' in some locations
原因は第12回で触れた通り、空の配列から始めているからです。中に何が入る予定なのか、TypeScriptには判断できません。
そこで第13回でやった type を使います。
type Item = {
name: string;
done: boolean;
};
let items: Item[] = [];
3行足すだけで、①の6件が消えます。しかも、この時点から item.name の打ち間違いが全部エラーになります。
③ 要素が null かもしれない問題
いちばん件数が多いのがこれです。
error TS18047: 'list' is possibly 'null'.
document.getElementById() は、該当する要素が無ければ null を返します。TypeScriptはその可能性を無視させてくれません。
1か所ずつ if で確認してもいいのですが、4回書くことになります。そこで確認する関数を1つ作ります。
function must(id: string): HTMLElement {
const el = document.getElementById(id);
if (el === null) {
throw new Error(`#${id} が見つかりません`);
}
return el;
}
const list = must("list");
const count = must("count");
const addButton = must("add");
must は「無ければその場で止める」関数です。戻り値の型が HTMLElement なので、受け取った側はもう null を気にせずに済みます。
嫁子: ここ、型のためだけに書いてるわけじゃないよ。
HTMLの
idを書き換えて、JavaScript側を直し忘れる事故ってよくあるでしょ。今までは画面が静かに動かなくなるだけだった。これからは「#list が見つかりません」と名指しで教えてくれるの。型に付き合わされてるように見えて、実は自分が助かってるんだよね。
④ input.value が使えない問題
3つ目です。
error TS2339: Property 'value' does not exist on type 'HTMLElement'
getElementById が返すのは「HTMLの要素」であって、それが入力欄かどうかまでは分かりません。value は入力欄だけが持つものなので、拒否されます。
よく見るのは as を使う書き方です。
const input = must("input") as HTMLInputElement;
as は「これは入力欄だと分かっているから、そう扱って」という指定です。ただし確認はしていません。もし違うものを指していたら、実行時に壊れます。
確認までするなら、こう書きます。
function mustInput(id: string): HTMLInputElement {
const el = must(id);
if (!(el instanceof HTMLInputElement)) {
throw new Error(`#${id} は入力欄ではありません`);
}
return el;
}
const input = mustInput("input");
instanceof は「本当にそれか」を実行時に確かめる書き方です。ここでは、こちらを使います。
嫁子:
asは、型チェックに対する「大丈夫だから通して」なの。第12回のanyと親戚だね。AIは
asをよく使うよ。手っ取り早いからね。それが間違っていたときに教えてくれるのは、もう誰もいないという点だけ覚えておいて。
⑤ 書き換え後の全体
3種類を片付けると、こうなります。これで npx tsc --noEmit がエラー0件で通ります。
type Item = {
name: string;
done: boolean;
};
let items: Item[] = [];
function must(id: string): HTMLElement {
const el = document.getElementById(id);
if (el === null) {
throw new Error(`#${id} が見つかりません`);
}
return el;
}
function mustInput(id: string): HTMLInputElement {
const el = must(id);
if (!(el instanceof HTMLInputElement)) {
throw new Error(`#${id} は入力欄ではありません`);
}
return el;
}
const list = must("list");
const count = must("count");
const input = mustInput("input");
const addButton = must("add");
function render(): void {
list.textContent = "";
items.forEach((item: Item, index: number) => {
const li = document.createElement("li");
const span = document.createElement("span");
span.textContent = item.name;
if (item.done) {
span.classList.add("done");
}
span.addEventListener("click", () => {
item.done = !item.done;
render();
});
const del = document.createElement("button");
del.textContent = "削除";
del.addEventListener("click", () => {
items.splice(index, 1);
render();
});
li.appendChild(span);
li.appendChild(del);
list.appendChild(li);
});
const remain = items.filter((item) => item.done === false).length;
count.textContent = `残り ${remain} 件`;
}
addButton.addEventListener("click", () => {
const name = input.value.trim();
if (name === "") {
return;
}
items.push({ name: name, done: false });
input.value = "";
render();
});
render();
変換して読み込むところまでは第15回と同じです。
npx tsc
HTML側は <script src="app.js"> のままで構いません。動くものは今まで通りのJavaScriptだからです。
⑥ 何が変わったのか
見た目も動きも同じですが、書ける間違いが減っています。
| 書いた間違い | 前(JavaScript) | 後(TypeScript) |
|---|---|---|
item.nane と打ち間違える |
画面に何も出ない | その場でエラー |
done に "yes" を入れる |
そのまま動く | その場でエラー |
HTMLの id を変えて直し忘れる |
静かに動かなくなる | 「見つかりません」と止まる |
items.push({ name: "牛乳" }) |
動くが done が無い |
項目が足りないとエラー |
最後の行が地味に効きます。あとから型に項目を1つ足すと、足し忘れている場所が全部エラーで出てきます。手で探す必要がありません。
嫁子: これが「後から直せるコード」の正体なの。第1回で「動くけどまずいコード」の話をしたでしょ。あそこで言ってた3つ目がこれだよ。
変更したときに、直すべき場所を機械が教えてくれるかどうか。それだけの違いなんだけど、半年後の自分にとっては天と地なんだよね。
⑦ AIに書き換えさせるなら
同じ作業をAIに頼むこともできます。そのときの頼み方です。
この
app.jsを TypeScript に書き換えてください。 -strictが有効な前提で、エラーが0件になるまで -anyとasは使わない -document.getElementByIdのnullは、確認用の関数を1つ作ってまとめる - データの型はtype Itemとして先に定義する
条件を書かないと、any と as で埋めた「エラーは無いが守られていない」コードが返ってくることがあります。今日やった3種類の分類は、そのまま指示に書く材料になります。
| 書いておく条件 | 書かないとどうなるか |
|---|---|
any を使わない |
型チェックが止まる |
as を使わない |
確認せずに通される |
| null の扱いを指定 | 同じ if が何度も書かれる |
嫁子: 自分で1回やったから、この条件が書けるようになったんだよ。
第10回でも同じことを言ったけど、AIに任せる前に一度自分でやる価値は、まさにここなの。やってない人は「エラーを消して」としか言えないからね。
つまずきポイント
items[index] のような番号での取り出しは、設定によっては「undefined かもしれない」と言われます。上のコードで forEach が渡してくれる item をそのまま使っているのは、そのためでもあります。
.js を編集しないでください。編集するのは .ts のほうです。.js は変換のたびに上書きされます。
今回のまとめ
- 拡張子を
.tsにすると、strictが一斉にチェックを始める(第10回のコードで14件) - エラーは種類ごとに分類する。今回は3種類だった
- 空の配列は
let items: Item[] = []で中身を宣言する getElementByIdのnullは、確認する関数を1つ作ってまとめるasは確認しない指定。instanceofなら実行時にも確かめられる- 型をつけると、あとから項目を足したとき、直す場所を機械が教えてくれる
次にやること
第3部(TypeScript)はこれで終わりです。次回から第4部・フロントエンドに入ります。
第9回の最後に書いた「手で全部書き換える方式の限界」を、ここから解決していきます。今日のコードには render() を呼び忘れると画面が古いまま残る問題がまだありますが、それを構造として解決するのがReactです。
今回の宿題:完成したコードの type Item に price: number; を1行足して、npx tsc --noEmit を実行してください。このアプリでは1か所(items.push のところ)が「price が足りない」と名指しで出ます。規模が大きくなるほど、この一覧の価値が上がります。
嫁子: 型を1行足しただけで、直すべき場所の一覧が出てくるの。これ、JavaScriptのままだったら自分で全部探すしかなかったんだよ。
第3部を通してやってきたことの意味が、たぶんこの瞬間にいちばんよく分かると思う。やってみてね。