昨天透過 Make this an app,Gemini 已經幫我建立出一個可以實際操作的 Learning Card App。
除了基本的學習卡之外,它還自己加入了語音朗讀、不同學習主題、挑戰模式、星星獎勵,甚至連親子互動提示都一起做了。
看起來已經很像一個完整的 Prototype。
不過如果先把這些功能放到旁邊,其實還有一個很基本的問題:
目前海洋動物卡的資料,還不是我們前面透過 Structured Output 產生的資料。
Day 12 明明已經成功讓 Gemini 回傳五張固定格式的海洋動物學習卡,Day 13 也建立了 TypeScript Type。
所以今天先不增加任何新功能。
我要做的事情只有一個:
把 Day 12 真正產生的五張 Learning Card,放進現在的 React UI。
Day 12 使用 Structured Output 後,我拿到的資料結構是:
{
"cards": [
{
"emoji": "🐢",
"title": "認識海龜",
"content": "海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
"question": "海龜背上背著什麼呢?",
"answer": "硬硬的大殼。"
}
]
}
每一張 Learning Card 都有五個欄位:
emoji
title
content
question
answer
Day 13 也把它整理成 TypeScript:
type LearningCard = {
emoji: string;
title: string;
content: string;
question: string;
answer: string;
};
原本我以為今天應該很簡單。
把 Day 15 的:
export const oceanCards = [...]
換成 Day 12 的五張資料就好了。
Copy、Paste、收工。
結果事情果然沒有這麼簡單(笑)。
我直接把 Day 12 的五張海洋動物資料放進:
src/data/learningCards.ts
結果 TypeScript 馬上出現錯誤:
Property 'id' is missing in type
'{ emoji: string; title: string; content: string; question: string; answer: string; }'
but required in type 'LearningCard'.(2741)
types.ts(2, 3): 'id' is declared here.
問題很明確:
Day 12 的資料少了
id。
奇怪的是,Day 13 的 LearningCard 明明沒有 id。
那這個 id 到底是哪裡來的?
回頭打開 Day 15 Gemini 建立的:
src/types.ts
才發現現在的 LearningCard 已經變成:
export interface LearningCard {
id: string;
emoji: string;
title: string;
pinyin?: string;
content: string;
question: string;
answer: string;
parentTip?: string;
soundName?: string;
}
跟 Day 13 放在一起比較就很明顯了。
Day 13:
type LearningCard = {
emoji: string;
title: string;
content: string;
question: string;
answer: string;
};
Day 15:
export interface LearningCard {
id: string;
emoji: string;
title: string;
pinyin?: string;
content: string;
question: string;
answer: string;
parentTip?: string;
soundName?: string;
}
也就是說,昨天 Gemini 使用 Make this an app 建立 Prototype 時,不只是幫我產生 UI 和 Component。
它還根據自己新增的功能,擴充了原本的資料模型:
id
pinyin
parentTip
soundName
這也讓我發現一件事情:
AI 幫我建立 App 的同時,也可能順便改變原本的 Data Contract。
如果只是看 Preview,這件事情其實很容易被忽略。
直到真正把自己的資料接進去,問題才浮現出來。
id 是 Required,所以不能少先看看:
id: string;
這個欄位後面沒有 ?,代表它是 Required。
因此:
const card: LearningCard = {
emoji: "🐢",
title: "認識海龜",
content: "...",
question: "...",
answer: "..."
};
就不符合現在的 LearningCard。
TypeScript 才會出現:
Property 'id' is missing
這也再次驗證 Day 13 做 TypeScript Type 的用途。
如果今天沒有:
LearningCard[]
這層型別檢查,我可能直接把資料貼進去,直到其他地方真的使用 id 時才發現問題。
既然少了 id,最直接的想法可能是:
那我回去 Day 12 的 Structured Output Schema 加一個
id不就好了?
不過這次我沒有這樣做。
因為目前的 id 比較像是 App 自己需要的識別資訊,不是 AI 要產生的學習內容。
我希望 Gemini 專心負責:
emoji
title
content
question
answer
至於:
id
則由應用程式自己處理。
所以今天先用最簡單的方法,在五張資料分別補上:
id: "ocean-1"
id: "ocean-2"
id: "ocean-3"
id: "ocean-4"
id: "ocean-5"
例如第一張變成:
{
id: "ocean-1",
emoji: "🐢",
title: "認識海龜",
content:
"海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
question: "海龜背上背著什麼呢?",
answer: "硬硬的大殼。"
}
補完五個 id 後,剛才的 TypeScript Error 就全部消失了。
這時候又出現另一個有趣的地方。
Day 15 的 LearningCard 還多了:
pinyin?: string;
parentTip?: string;
soundName?: string;
但 Day 12 的資料完全沒有這些欄位。
為什麼 TypeScript 沒有繼續報錯?
答案就在欄位後面的:
?
這三個欄位都是 Optional:
pinyin?: string;
parentTip?: string;
soundName?: string;
代表它們可以存在,也可以不存在。
所以現在這種資料:
{
id: "ocean-1",
emoji: "🐢",
title: "認識海龜",
content: "海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
question: "海龜背上背著什麼呢?",
answer: "硬硬的大殼。",
}
符合 LearningCard。✓
Day 15 原本這種比較完整的資料:
{
id: "safari-1",
emoji: "🦁",
title: "獅子王",
pinyin: "shī zi",
content: '公獅子頭上有厚厚茂密的鬃毛,吼一聲「吼——」聲音好威武!',
question: '獅子頭上有什麼毛茸茸的呢?',
answer: '茂盛的鬃毛!',
parentTip: '讓小朋友手圍在臉龐學獅子大吼一聲「吼~~」!',
soundName: '吼~~~!'
}
也一樣符合 LearningCard。✓
這次終於不是只在 TypeScript 裡看 ? 了,而是真的在 App 裡看到 Optional Data 的效果。
把五張海洋動物卡換成 Day 12 的資料之後,Preview 成功執行。
原本 Day 15 是:
海洋探險 1 / 7
現在則變成:
海洋探險 1 / 5
第一張也從原本 Gemini 自己產生的藍鯨,變成 Day 12 的:
🐢 認識海龜
接著按「下一張」,也可以依序看到:
🐢 認識海龜
↓
🐋 認識鯨魚
↓
🐙 認識章魚
↓
🦀 認識螃蟹
↓
🐠 認識小丑魚
也就是說,Day 12 的五張 Learning Card 已經真的進到 React UI 裡了。
pinyin、parentTip、soundName 呢?實際操作後,我也注意到一個差異。
因為 Day 12 的海洋動物資料沒有:
pinyin
parentTip
soundName
所以切換到海洋動物時,UI 上也不會顯示這些欄位的內容。

一開始看到少了一些東西,我還想確認是不是換資料時把 Component 弄壞了。
於是我再切換到其他沒有修改的主題。
例如原本的森林動物資料仍然包含:
pinyin: "shī zi",
parentTip: "...",
soundName: "..."
結果這些內容在 UI 上依然正常顯示。

所以 Component 本身沒有壞掉。
差別真的只是:
海洋動物
Day 12 Data
↓
沒有 Optional Data
↓
不顯示相關內容
而其他未修改的資料:
森林動物等
Day 15 Data
↓
有 Optional Data
↓
正常顯示相關內容
這個結果剛好讓 Required 與 Optional 的差異變得非常直觀。
把今天的結果整理一下:
id 是:
id: string;
所以:
沒有 id
↓
不符合 LearningCard
↓
TypeScript TS2741 ❌
但:
pinyin?: string;
parentTip?: string;
soundName?: string;
是 Optional,因此:
沒有這些欄位
↓
仍符合 LearningCard
↓
TypeScript ✓
↓
UI 不顯示相關內容
如果資料有提供:
有這些欄位
↓
仍符合 LearningCard
↓
TypeScript ✓
↓
UI 顯示相關內容
前面在設計 Schema 和 Type 的時候,Optional 還比較像一個資料結構上的概念。
今天真的把資料丟進 UI 後,才開始看到它對實際畫面的影響。
今天沒有新增什麼厲害的新功能。
甚至從程式碼來看,主要工作只是:
換掉
oceanCards,再補上五個id。
但這一步對整個系列來說反而滿重要的。
因為前面幾天原本是各自分開的:
Day 12
Structured Output
↓
Learning Card JSON
Day 13
TypeScript
↓
LearningCard Type
Day 14~15
React
↓
Learning Card UI
到了今天,終於真的接起來:
Structured Output
↓
Day 12 Learning Card JSON
↓
補上 App 需要的 id
↓
LearningCard[]
↓
Day 15 React UI
↓
五張學習卡成功顯示
現在畫面上的海洋動物資料,終於不再是 Make this an app 為了 Prototype 自己補出來的 Mock Data,而是我們前面真的透過 Structured Output 產生過的五張 Learning Card。
今天原本以為只是 Copy & Paste 資料,結果反而遇到了一個很典型的前端問題:
Data Contract 對不上。
Day 12 的 Structured Output 定義了 AI 要產生什麼資料;Day 15 的 Make this an app 則因為自己加入更多功能,擴充了 React App 所需要的資料。
兩邊第一次真正接起來時,TypeScript 就幫我抓到了 id 不存在的問題。
補上 id 後,五張 Learning Card 成功顯示;而 pinyin、parentTip、soundName 因為本來就是 Optional,即使沒有提供也不影響 TypeScript,只會影響對應 UI 是否有內容可以顯示。
所以今天最大的收穫不是「成功換了五張卡」,而是開始真正理解:
AI 回傳的資料格式,不一定會完全等於 App 最後使用的資料格式。
中間可能還需要一層轉換與整理。
今天我只是手動補:
id: "ocean-1"
但等真正串上 Gemini API 後,這件事情當然不能每次都手動做。
而且現在還有另一個問題。
如果我明天突然不想學海洋動物,而是想產生:
3~4 歲
恐龍
5 張
難道又要重新修改 learningCards.ts 嗎?
這樣好像還不能叫「智慧學習卡產生器」。
目前學習對象、主題和卡片資料都還寫在程式裡。
明天先把這些條件搬到畫面上,做一個簡單的 Learning Card Generator Form:
學習對象:3~4 歲
主題:海洋動物
卡片數量:5
[ 產生學習卡 ]
先讓使用者可以告訴 App:
「今天想學什麼?」
等輸入介面準備好之後,下一步就可以開始把它真正送給 Gemini。