型ガードで絞り込め
中段 / 目安 25分
凛さんがモニターに障害報告を2件並べました。「先週、同じ事故が2回ありました。findItem は 見つからないこともある 関数です。戻り値は Item | undefined。なのに、返ってきたものをそのまま .name と触ったコードが本番に出ていました」
undefined.name は実行時に落ちます。ページが真っ白になり、ユーザーには何が起きたのかわかりません。しかし TypeScript を strict で使っているなら、この事故はコンパイルの時点で止められます。Item | undefined という型は、まさに「undefined かもしれない」と宣言しているからです。
ここで効くのが narrowing (絞り込み) です。TypeScript は if 文の条件を読んで、その先で値がどの型になっているかを勝手に狭めてくれます。if (!item) return ... と早期 return を書くと、その行より下では item は Item に確定します。undefined の可能性は、return によって既に分岐の外へ追い出されているからです。
凛さんは言います。「絞り込みは、コードを読む人へのメッセージでもあります。関数の頭で危険な入口を塞いでおけば、残りの行はもう安全な世界です。読む側は身構えなくていい」
この問題では、絞り込みを 書かないと本当に型エラーになる ことを確かめてください。strictNullChecks が効いているので、ガードを消すと「'item' は 'undefined' の可能性があります」という赤が出ます。エラーが出たら、それは失敗ではなく、コンパイラが事故を先回りしてくれた証拠です。
完成条件
describeItem という関数を1つ定義してください。
引数は2つです。1つ目 items は商品の配列、2つ目 id は探したい商品の番号です。商品1件は次の3つを持ちます。
id— 番号 (number)name— 商品名 (string)price— 価格 (number)
この形を Item という interface として定義してください。そのうえで、次の2つを作ります。
findItem(items, id)— id が一致する商品を返す。見つからなければundefinedを返す。戻り値の型はItem | undefinedと明記してくださいdescribeItem(items, id)—findItemを呼び、早期 return で絞り込んでから 説明文を組み立てて返す
返す文字列は次の2つの形です。
- 見つかったとき —
深煎りブレンド 480円 - 見つからなかったとき —
該当なし
名前と価格のあいだは半角スペース1つ、価格のうしろに 円 を付けます。
書けたら1つ実験してください。if (!item) return "該当なし"; の行を消してみると、その下の item.name が赤くなるはずです。その赤が、先週の事故そのものです。確かめたら戻してください。
進め方
白紙から書いてください。手が止まったらヒントを開けます。ヒントは3段階で、①方針、②使う構文、③部分解の順に出ます。1つも開けずに通せたら、その問題は自分のものです。
開かずに10分粘って進まないなら、①だけ開けてください。粘る時間そのものには価値がありません。価値があるのは、自分で書いた行が動いた瞬間です。
要件
- interface Item を定義する。持つのは id(number)・name(string)・price(number) の3つ
- findItem(items: Item[], id: number): Item | undefined を定義し、戻り値の型を明記する
- describeItem(items: Item[], id: number): string を定義する
- describeItem は findItem の戻り値を早期 return で絞り込んでから使うこと
- 見つかったときは「深煎りブレンド 480円」の形の文字列を返す
- 見つからなかったときは「該当なし」を返す
- any を使わないこと
入出力例
describeItem([{"id":1,"name":"深煎りブレンド","price":480},{"id":2,"name":"抹茶ラテ","price":580}], 1) → "深煎りブレンド 480円"
describeItem([{"id":1,"name":"深煎りブレンド","price":480},{"id":2,"name":"抹茶ラテ","price":580}], 2) → "抹茶ラテ 580円"
describeItem([{"id":1,"name":"深煎りブレンド","price":480},{"id":2,"name":"抹茶ラテ","price":580}], 9) → "該当なし"
describeItem([], 1) → "該当なし"
describeItem([{"id":7,"name":"自家製レモネード","price":560},{"id":8,"name":"チーズケーキ","price":480},{"id":9,"name":"季節のフルーツティー","price":620}], 9) → "季節のフルーツティー 620円"ヒント
前のヒントを開くと次が開きます。開かずに解けると未開封クリアです