1. test 1
  2. test 2
  3. test 3
  4. test 4
  5. test 5
コース一覧
TypeScript道場 fleama開発室の10問
総合 型のないモジュールを完全TS化

TypeScript道場 fleama開発室の10問

TypeScript入門で身に付けた技を、商店街公式フリマアプリ「fleama」開発室の仕事で試す演習専用コースです。解説はありません。依頼と完成条件だけを読み、白紙から自分で組み立てます。手が止まったときのために、方針・使う構文・部分解の3段階のヒントを各問に用意しています。判定はコンパイラそのものです。型が通らないコードは実行される前に不合格になるので、まず tsc を黙らせてから中身を合わせることになります。基本型の注釈から始まり、関数の署名、any の撲滅、interface の設計、リテラルunion、型ガード、ジェネリクス、入口での変換、入れ子のAPIレスポンスを経て、最後は型のないモジュールを strict で通る形に組み直すところまで、10問で TypeScript の主要な道具を一巡します。1問10分から45分、全10問で約4.5時間です。TypeScript入門を終えて「読めるけれど自分では書けない」と感じている方に向いています。

1
初段
01. 赤い波線を消せ10分
02. 関数に正しい型を付けよ15分
03. このanyを撲滅せよ15分
04. Item型を設計せよ20分
2
中段
01. 不正な状態を型で封じよ25分
02. 型ガードで絞り込め25分
03. 汎用関数に型を付けよ30分
04. 型を付けて事故を防げ30分
3
師範
01. APIレスポンスに型を通せ35分
02. 総合 型のないモジュールを完全TS化45分

総合 型のないモジュールを完全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つ定義します。

  1. toPrice(value: number | string): number — 数値ならそのまま、文字列なら数値へ。変換できないときは 0
  2. toStatus(value: string): ItemStatus — "on_sale" "sold" "draft" のどれかならその値を、それ以外なら "draft" を返す
  3. normalize(raws: RawItem[]): Item[] — 生のデータを1件ずつ掃除して Item の配列にする
  4. countBy<T, K extends string>(list: T[], toKey: (value: T) => K): Partial<Record<K, number>> — 配列を、渡された関数が返すキーごとに数え上げる汎用の関数。該当が0のキーは入れません。戻り値が Partial なのは、キーが無いことがあるからです。この1語が「取り出したら undefined かもしれない」と呼ぶ側に伝えます
  5. 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 は、今日も静かに緑です。

要件

  1. type ItemStatus を "on_sale" | "sold" | "draft" のリテラル union で定義する
  2. interface RawItem を定義する。name(string)・price(number | string)・status(string)
  3. interface Item を定義する。name(string)・price(number)・status(ItemStatus)
  4. toPrice(value: number | string): number を定義する。変換できないときは 0
  5. toStatus(value: string): ItemStatus を定義する。3つのどれでもなければ "draft"
  6. normalize(raws: RawItem[]): Item[] を定義する
  7. countBy<T, K extends string>(list: T[], toKey: (value: T) => K): Partial<Record<K, number>> を定義し、該当0のキーは入れない
  8. report(raws: RawItem[]): string を定義し、販売中の件数は countBy を使って数える
  9. 売上合計は status が sold のものの price の合計。最高値は販売中のうち price が最大のものの名前で、同値なら先に出てきたほう
  10. 販売中が1件も無ければ最高値は「なし」と書く
  11. 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円 / 最高値 チーズケーキ"

ヒント

前のヒントを開くと次が開きます。開かずに解けると未開封クリアです

①方針 5段で進めます。まず型を3つ書きます。次に toPrice と toStatus という小さな掃除係を書いて、単体で正しく動くか確かめます。次に normalize でその2つを使って配列を作り、次に countBy を単体で作り、最後に report で全部を呼び出して文字列を組み立てます。report から書き始めないでください

ヒント 2ヒント 1 を開くと読めます
ヒント 3ヒント 2 を開くと読めます
生田 陸人
監修生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア監修者プロフィールを見る →
編集 ゆめさく編集部·公開 2026/08/09

関連レッスン

  • APIレスポンスに型を通せ

    入れ子になったAPIレスポンスを、as を使わずに型の入れ子で表します。通信の向こう側まで型を届かせます。

このレッスンに出てくる用語

意味があいまいなまま進んだ語は、ここから読み直せます。

  • TypeScript静的型を加えた JavaScript の上位互換
  • マージブランチを統合する操作。
  • 型注釈変数や引数に : 型 で宣言する書式
  • 関数処理に名前を付けて再利用できる単位
  • interface実装を持たないメソッド契約。複数 implements 可
  • ジェネリクス<String> のように型を指定する仕組み
  • 正規化データの重複を減らし整合性を保つ設計手法
  • モジュール他ファイルから import できるPythonファイル
main.ts
学習モード
エディタを読み込んでいます

メモ

総合 型のないモジュールを完全TS化

⌘S で保存