iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

昨天我們終於讓 Gemini 一次回傳 5 張 Learning Cards,而且資料不再是一大段文字,而是有固定結構的 JSON:

{
  "cards": [
    {
      "emoji": "🐢",
      "title": "認識海龜",
      "content": "...",
      "question": "...",
      "answer": "..."
    }
  ]
}

做到這裡,Gemini 已經知道 Learning Card 應該長什麼樣子了。

但身為前端工程師,看到這份 JSON 之後,我腦中馬上又出現另一個問題:

Gemini 知道資料格式了,那 TypeScript 知道嗎?

所以今天先不碰 Prompt,也不繼續調整 Gemini,而是回到比較熟悉的 TypeScript 世界。


從 JSON Schema 到 TypeScript

前幾天在 Structured Output 裡,我們定義了一張 Learning Card 需要包含:

emoji
title
content
question
answer

而且這五個欄位都是 string。

到了 TypeScript,其實可以直接把同樣的資料結構定義成:

type LearningCard = {
  emoji: string;
  title: string;
  content: string;
  question: string;
  answer: string;
};

這樣一來,TypeScript 就知道:

一筆 LearningCard 應該有哪些欄位,以及每個欄位是什麼型別。

把前幾天的 Schema 和今天的 TypeScript 放在一起看,其實兩邊正在描述同一份資料:

Gemini Schema             TypeScript

emoji: string       →     emoji: string
title: string       →     title: string
content: string     →     content: string
question: string    →     question: string
answer: string      →     answer: string

只是它們負責的階段不同。

Structured Output 的 Schema 用來定義 Gemini 應該產生什麼樣的資料結構;TypeScript Type 則讓程式知道自己預期使用什麼樣的資料。


別忘了外面的 cards

昨天我們最後設計的 Response 並不是直接回傳 Learning Card Array,而是:

{
  "cards": [...]
}

因此除了單張卡片的 LearningCard,還需要再定義整個 Response:

type LearningCardResponse = {
  cards: LearningCard[];
};

這裡的:

LearningCard[]

就是由多個 LearningCard 組成的 Array。

所以 Day 12 的資料結構:

Response
└── cards: Array
    └── Learning Card Object

到了 TypeScript 就變成:

LearningCardResponse
└── cards: LearningCard[]

昨天才剛處理完 Array,今天馬上就派上用場了。


把 Gemini 的 JSON 放進 TypeScript

接下來我直接把昨天 Gemini 產生的資料放進 index.ts,並指定它的型別為 LearningCardResponse:

type LearningCard = {
    emoji: string;
    title: string;
    content: string;
    question: string;
    answer: string;
};

type LearningCardResponse = {
    cards: LearningCard[];
};

const response: LearningCardResponse = {
    cards: [
        {
            emoji: "🐢",
            title: "認識海龜",
            content:
                "海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
            question: "海龜背上背著什麼呢?",
            answer: "硬硬的大殼。",
        },
        {
            emoji: "🐋",
            title: "認識鯨魚",
            content:
                "鯨魚是世界上體型最大的動物,牠在水面上呼氣的時候,頭頂會噴出高高的水花喔!",
            question: "鯨魚呼氣時,頭頂會噴出什麼呢?",
            answer: "高高的水花。",
        },
        {
            emoji: "🐙",
            title: "認識章魚",
            content:
                "章魚的身體軟綿綿的,牠有八隻長長的腳,遇到危險時還會噴出黑黑的墨汁逃跑喔!",
            question: "章魚有幾隻長長的腳呢?",
            answer: "八隻腳。",
        },
        {
            emoji: "🦀",
            title: "認識螃蟹",
            content:
                "螃蟹有硬硬的外殼和一對像剪刀的大鉗子,而且牠們走路時都是橫著走的喔!",
            question: "螃蟹走路是向前走還是橫著走呢?",
            answer: "橫著走。",
        },
        {
            emoji: "🐠",
            title: "認識小丑魚",
            content:
                "小丑魚身上有漂亮橘色和白色的條紋,最喜歡躲在軟綿綿的海葵家裡面玩捉迷藏喔!",
            question: "小丑魚最喜歡躲在哪裡呢?",
            answer: "軟綿綿的海葵家裡。",
        },
    ],
};

console.log(response);

接著使用 TypeScript compiler 做型別檢查:

npx tsc --noEmit index.ts

這裡的 --noEmit 可以簡單理解成:

幫我做 TypeScript 型別檢查,但先不要輸出編譯後的 JavaScript。

環境準備好之後,就可以開始今天的三組小實驗。


Run #1|正常資料

第一輪先維持正常的 Learning Card:

{
  emoji: "🐢",
  title: "認識海龜",
  content: "海龜住在美麗的大海裡...",
  question: "海龜背上背著什麼呢?",
  answer: "硬硬的大殼。",
}

執行:

npx tsc --noEmit index.ts

Terminal 沒有出現任何 TypeScript Error。

也就是:

LearningCard
├── emoji       ✓
├── title       ✓
├── content     ✓
├── question    ✓
└── answer      ✓

No TypeScript errors

第一關順利通過。


Run #2|如果少了 answer 呢?

第二輪開始故意把資料弄壞看看。

我把第一張 Learning Card 的 answer 拿掉:

{
  emoji: "🐢",
  title: "認識海龜",
  content: "海龜住在美麗的大海裡...",
  question: "海龜背上背著什麼呢?",
}

但原本定義的 LearningCard 仍然是:

type LearningCard = {
  emoji: string;
  title: string;
  content: string;
  question: string;
  answer: string;
};

這時 VS Code 馬上出現紅色波浪線,實際收到的錯誤是:

Property 'answer' is missing in type
'{ emoji: string; title: string; content: string; question: string; }'
but required in type 'LearningCard'. ts(2741)

'answer' is declared here.

TypeScript 很直接地告訴我:

LearningCard 規定需要 answer,但是目前這筆資料沒有。

也就是:

LearningCard
├── emoji       ✓
├── title       ✓
├── content     ✓
├── question    ✓
└── answer      ✗ Missing

看到這裡,很容易聯想到 Day 11 在 Schema 裡設定的:

"required": [
  "emoji",
  "title",
  "content",
  "question",
  "answer"
]

Structured Output 用 required 描述 Gemini 輸出時哪些欄位必須存在;到了 TypeScript,我們則透過 LearningCard 定義程式預期有哪些欄位。


Run #3|欄位有了,但型別錯了呢?

第三輪再換一種方式。

這次不刪除 answer,而是把原本的:

answer: "硬硬的大殼。",

故意改成:

answer: 123,

answer 還在,但現在從 string 變成了 number。

TypeScript 這次出現:

Type 'number' is not assignable to type 'string'. ts(2322)

The expected type comes from property 'answer'
which is declared here on type 'LearningCard'

原因也很明確。

我們定義的是:

answer: string;

實際給的卻是:

answer: 123;

所以現在變成:

answer

預期:string
實際:number
        ↓
TypeScript Error

這次 TypeScript 不只知道 Learning Card「有哪些欄位」,還知道:

每個欄位應該是什麼資料型別。


三次實驗放在一起看

今天三次實驗的結果就很清楚了:

實驗 answer TypeScript
Run #1 "硬硬的大殼。" ✅ No errors
Run #2 缺少 answer ❌ Property answer is missing
Run #3 123 ❌ number is not assignable to string

所以建立 Type 的目的,不只是幫 JSON 取一個叫 LearningCard 的名字。

我們其實是在告訴 TypeScript:

LearningCard
│
├── 要有哪些欄位?
│
└── 每個欄位是什麼型別?

當程式裡使用的資料不符合這份定義時,TypeScript 就能在開發階段提醒我們。

這也是我平常寫 TypeScript 時很習慣的一件事,但放到這次 Gemini 的資料流程裡,就更能看出它的位置。


Schema 跟 TypeScript 各自負責什麼?

做到這裡,前幾天的 Structured Output 和今天的 TypeScript 終於可以串起來了。

目前整個流程大概是:

Learning Requirement
        ↓
      Gemini
        ↓
Structured Output Schema
        ↓
    JSON Response
        ↓
LearningCardResponse
        ↓
  LearningCard[]

Schema 和 TypeScript 看起來都在描述資料結構,但負責的事情不太一樣。

可以先簡單理解成:

Structured Output
→ 定義 Gemini 應該產生什麼樣的資料結構

TypeScript
→ 定義前端程式預期怎麼使用這些資料

前面幾天一直在處理「AI 怎麼回資料」,今天則開始進入「程式怎麼接這份資料」。

終於慢慢回到前端工程師的主場了(笑)。


但 TypeScript Type 不等於 Runtime Validation

這裡還有一件事情不能搞混。

今天建立:

type LearningCardResponse = {
  cards: LearningCard[];
};

並不代表以後從 Gemini API 回來的任何資料,TypeScript 都會自動幫我們確認它一定符合這個結構。

例如之後真的開始串 API:

const response = await fetch("...");
const data = await response.json();

這些資料是在程式執行時才從外部取得的。

而今天建立的 TypeScript Type,主要是在開發階段幫助我們描述與檢查程式中的資料。

所以:

TypeScript Type
≠
Runtime Validator

今天先知道這個差別就好。

至於要不要再做 Runtime Validation……

這個坑今天先不要挖,不然只是想做五張學習卡,最後可能連 Zod 都跑出來了(笑)。


Day 13 小結

今天沒有繼續修改 Prompt,也沒有重新測試 Gemini,而是把昨天取得的 JSON 正式帶進 TypeScript。

我們建立了:

type LearningCard = {
  emoji: string;
  title: string;
  content: string;
  question: string;
  answer: string;
};

type LearningCardResponse = {
  cards: LearningCard[];
};

並且實際做了三次測試。

正常資料可以順利通過型別檢查;少了必要的 answer,TypeScript 會提醒欄位缺失;把 answer 從 string 改成 number,也會直接告訴我們型別不符合。

從 Day 09 開始,我們一路從一段「人看得懂的文字」,慢慢整理成:

文字
 ↓
JSON
 ↓
Schema
 ↓
Array
 ↓
TypeScript Type

現在 Learning Card 已經不只是一段 Gemini 產生的 JSON,而是開始變成前端程式真正可以理解與使用的資料。

照以前的開發方式,下一步大概就是開一個 React 專案,開始建 Component、寫樣式,再慢慢把 Learning Card 畫面刻出來。

但都已經做到「Build on Google AI」了,我突然想到另一個問題:

現在的第一版 UI,真的還需要全部自己從零開始刻嗎?

既然 AI 可以幫我們產生學習內容,那前端 UI 是不是也可以先交給 AI 試試看?

Day 14|現在還需要自己刻前端嗎?讓 AI 幫我建立第一版 Learning Card UI


上一篇
Day 12|一張不夠,那就一次來五張:讓 Gemini 回傳 Learning Card Array
下一篇
Day 14|現在還需要自己刻前端嗎?讓 AI 幫我建立第一版 Learning Card UI
系列文
今天學什麼?30 天用 Google AI 打造智慧學習卡 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言