総合 型のないモジュールを完全TS化
師範 / 目安 45分
凛さんが、古いファイルを1つ開きました。ファイル名は utils.js。先代のエンジニアが残していったものです。
「動いています。3年動いています。それでも誰も触りません。何を受けて何を返すのか、読まないと分からないからです」
凛さんはそこで、初めて少しだけ笑いました。「最終試験です。これを完全な TypeScript にしてください。strict設定で私のCIが緑になったら、あなたを正式メンバーとしてマージします」
ここまでの9問が全部出ます。基本型注釈、関数の署名、any の撲滅、interface での形の宣言、リテラル union、絞り込み、ジェネリクス、入口での正規化、入れ子の型。新しく覚える構文はありません。持っている道具を、正しい順番で当てるだけです。
順番には定石があります。データの型から書き、次に関数の署名を書き、中身は最後です。中身から書き始めると、型を1つ直すたびに中身も直すことになり、いつまでも終わりません。外側の形が決まってしまえば、中身は形に従うだけです。
凛さんが最後の助言をくれました。「エラーが20個出ても、慌てないでください。上から1つずつ消してください。上のエラーが下のエラーを生んでいることが多いので、1つ直すと5つ消えます。数を見るのではなく、いちばん上の1行を見てください」
完成条件
出品管理モジュールを、strict で通る TypeScript として書き上げてください。
まず型を3つ定義します。
ItemStatus—"on_sale""sold""draft"の3つだけを許すリテラル union 型RawItem— 外から来る生の出品データ。name(string)、price(number または string)、status(string)Item— 掃除済みの出品データ。name(string)、price(number)、status(ItemStatus)
次に関数を5つ定義します。
toPrice(value: number | string): number— 数値ならそのまま、文字列なら数値へ。変換できないときは0toStatus(value: string): ItemStatus—"on_sale""sold""draft"のどれかならその値を、それ以外なら"draft"を返すnormalize(raws: RawItem[]): Item[]— 生のデータを1件ずつ掃除してItemの配列にするcountBy<T, K extends string>(list: T[], toKey: (value: T) => K): Partial<Record<K, number>>— 配列を、渡された関数が返すキーごとに数え上げる汎用の関数。該当が0のキーは入れません。戻り値がPartialなのは、キーが無いことがあるからです。この1語が「取り出したら undefined かもしれない」と呼ぶ側に伝えますreport(raws: RawItem[]): string— 上の4つを使って、締めの報告を1つの文字列にして返す
report が返す文字列は次の形です。
プレーンテキスト
出品3件 / 販売中2件 / 売上合計 1060円 / 最高値 深煎りブレンド組み立ての決まりは次のとおりです。
出品は掃除後の全件数です販売中はstatusが"on_sale"のものの件数です。countByを使って数えてください売上合計はstatusが"sold"のもののpriceの合計です。1件も無ければ0円です最高値は 販売中のものだけを対象に、priceがいちばん高い商品の名前です。同じ価格が並んだときは 先に出てきたほうを選びます。販売中が1件も無ければなしと書きます- 区切りは半角スペースを挟んだ
/です - 全件が空のときは
出品0件 / 販売中0件 / 売上合計 0円 / 最高値 なしになります
any と as は使わないでください。toStatus で文字列を ItemStatus に変えるとき、as に手が伸びます。伸びたら、それは絞り込みで解ける合図です。比較で1つずつ確かめれば、TypeScript はちゃんと ItemStatus として認めてくれます。
進め方
白紙から書いてください。手が止まったらヒントを開けます。ヒントは3段階で、①方針、②使う構文、③部分解の順に出ます。1つも開けずに通せたら、その問題は自分のものです。
開かずに10分粘って進まないなら、①だけ開けてください。粘る時間そのものには価値がありません。価値があるのは、自分で書いた行が動いた瞬間です。
一息に書こうとすると必ず崩れます。型3つを書く、toPrice と toStatus を書いて動かす、normalize を書いて掃除できたか確かめる、countBy を書く、最後に report で組み立てる。この5段で進めてください。道場で何度も作ってきた形です。
凛さんがターミナルを見つめています。CI のログが流れ、最後の1行が緑になりました。
「──Approved。ようこそ、fleama開発室へ」
差し出された紙には TypeScript道場・師範代を允許する と書かれていました。赤い波線を1本消すところから始めて、いまあなたは型の無いモジュールを、事故の起きない形に組み直せます。型は書く手間ではなく、未来の自分と仲間への手紙です。凛さんの CI は、今日も静かに緑です。
要件
- type ItemStatus を "on_sale" | "sold" | "draft" のリテラル union で定義する
- interface RawItem を定義する。name(string)・price(number | string)・status(string)
- interface Item を定義する。name(string)・price(number)・status(ItemStatus)
- toPrice(value: number | string): number を定義する。変換できないときは 0
- toStatus(value: string): ItemStatus を定義する。3つのどれでもなければ "draft"
- normalize(raws: RawItem[]): Item[] を定義する
- countBy<T, K extends string>(list: T[], toKey: (value: T) => K): Partial<Record<K, number>> を定義し、該当0のキーは入れない
- report(raws: RawItem[]): string を定義し、販売中の件数は countBy を使って数える
- 売上合計は status が sold のものの price の合計。最高値は販売中のうち price が最大のものの名前で、同値なら先に出てきたほう
- 販売中が1件も無ければ最高値は「なし」と書く
- any と as を使わないこと
入出力例
report([{"name":"深煎りブレンド","price":480,"status":"on_sale"},{"name":"抹茶ラテ","price":"580","status":"sold"},{"name":"カフェオレ","price":420,"status":"on_sale"}]) → "出品3件 / 販売中2件 / 売上合計 580円 / 最高値 深煎りブレンド"
report([]) → "出品0件 / 販売中0件 / 売上合計 0円 / 最高値 なし"
report([{"name":"チーズケーキ","price":"480","status":"sold"},{"name":"季節のフルーツティー","price":620,"status":"sold"},{"name":"自家製レモネード","price":"unknown","status":"うりきれ"}]) → "出品3件 / 販売中0件 / 売上合計 1100円 / 最高値 なし"
report([{"name":"手挽きミル","price":3200,"status":"on_sale"},{"name":"ドリップポット","price":3200,"status":"on_sale"},{"name":"古書 珈琲の本","price":"1200","status":"draft"},{"name":"豆缶","price":900,"status":"sold"}]) → "出品4件 / 販売中2件 / 売上合計 900円 / 最高値 手挽きミル"
report([{"name":"季節のフルーツティー","price":"620","status":"on_sale"},{"name":"抹茶ラテ","price":580,"status":"unknown"},{"name":"深煎りブレンド","price":"480","status":"sold"},{"name":"カフェオレ","price":"520","status":"sold"},{"name":"チーズケーキ","price":1480,"status":"on_sale"}]) → "出品5件 / 販売中2件 / 売上合計 1000円 / 最高値 チーズケーキ"ヒント
前のヒントを開くと次が開きます。開かずに解けると未開封クリアです