目前我們的 Todo 元件是放在 App.tsx,因此先到 App.tsx 放入我們要傳遞的 props:
import './App.css'
import Todo from './components/Todo'
function App() {
return (
<main>
<Todo content='Learn typeScript' isFinished={false} />
</main>
)
}
export default App
接著我們回到 Todo.tsx 使用解構的方式獲取 props,並將原本寫死的 Content 以及 isFinished 換成傳入的 props。
注意!傳入的 props 是一個物件,這是 React 的機制,而非 TypeScript 的特殊語法:
export default function Todo({ content, isFinished }) {
return (
<div className='flex items-center gap-[20px] justify-between mb-3'>
<input type='checkbox' checked={isFinished} />
<p>{content}</p>
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
到目前為止都與我們原先開發 React 方式相同,唯一不同的部分,就是我們需要為傳入的 props 定義型別,經過定義型別後,你會發現 IDE 不再報錯:
export default function Todo({
content,
isFinished,
}: {
content: string
isFinished: boolean
}) {
return (
<div className='flex items-center gap-[20px] justify-between mb-3'>
<input type='checkbox' checked={isFinished} />
<p>{content}</p>
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}

雖然我們上面的做法並不會有問題,但是當傳入的 props 內容變多時,將會影響程式碼的閱讀性。
還記得我們在 Day07 的時候介紹的 Custom Types & Interface 嗎?
我們可以將先前學到的型別定義方法應用到 React 中。
使用 type 將型別提取出來,再將該型別放入 props:
type TodoProps = { content: string; isFinished: boolean }
export default function Todo({ content, isFinished }: TodoProps) {
return (
<div className='flex items-center gap-[20px] justify-between mb-3'>
<input type='checkbox' checked={isFinished} />
<p>{content}</p>
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
使用 Interface 也是完全沒有問題的,這邊需要注意的是,如同我們先前提過的,使用 Interface 定義型別時,不需像使用 Custom Types 那樣在中間放入等號:
interface TodoProps {
content: string
isFinished: boolean
}
export default function Todo({ content, isFinished }: TodoProps) {
return (
<div className='flex items-center gap-[20px] justify-between mb-3'>
<input type='checkbox' checked={isFinished} />
<p>{content}</p>
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
相信 React 開發者們對於 children 這個 props 肯定不陌生,當元件包覆了其他內容,這個內容就會是該元件的 children,首先,我們先到 App.tsx 把 content 改為 children 的方式傳入:
import './App.css'
import Todo from './components/Todo'
function App() {
return (
<main>
<Todo isFinished={false}>
<p>Learn typeScript</p>
</Todo>
</main>
)
}
export default App
接著我們回到 Todo.tsx 為 children 定義型別,你認為 children 的型別是什麼呢?因為我們的內容是 Learn typeScript,所以是 string 嗎?
我們都知道在 React 元件中 return 的內容會是 JSX,所以我們不能直接定義為 string。
在這邊要提到一個特別的型別,它叫做 ReactNode,ReactNode 是 React 的型別定義之一,已經隨 React 工具包安裝在專案內,它的用途是用來表示任何可以作為 React 元件子元素的內容,而不僅限於字串,還包括數字、元素、陣列等
打開 package.json,你會在 devDependencies 裡看到以下這兩個工具,這些工具使我們可以處理一些 React 中的特別型別:
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
若我們要將原本的 content 改為使用 children 引入,我們需要使用 ReactNode 定義型別,並將原本的 content 內容改為 children,在 App.tsx 中,我們已經為 children 加上了 <p> tag,因此在這邊我們可以直接用 {children} 取代原本的 <p>{content}</p>:
import { type ReactNode } from 'react'
interface TodoProps {
isFinished: boolean
children: ReactNode
}
export default function Todo({ isFinished, children }: TodoProps) {
return (
<div className='flex items-center gap-[20px] justify-between mb-3'>
<input type='checkbox' checked={isFinished} />
{children}
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
為了不需要每次都手動為 children 指定型別,React 提供了 PropsWithChildren 泛型工具,它預設將 children 視為 ReactNode:
import { type PropsWithChildren } from 'react'
// interface TodoProps {
// isFinished: boolean
// }
type TodoProps = PropsWithChildren
export default function Todo({ isFinished, children }: TodoProps) {
return (
<div className='flex items-center gap-[20px] justify-between mb-3'>
<input type='checkbox' checked={isFinished} />
{children}
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
把滑鼠移到 {children} 上面,你會看到 IDE 提示 (parameter) children: React.ReactNode,這與我們先前指定的型別是一致的:
程式碼報錯的原因是因為我們尚未為 isFinished 指定型別。將滑鼠移到 PropsWithChildren 上時,IDE 會提示我們:PropsWithChildren 是一個泛型工具,允許我們傳遞其他型別作為參數。接下來,我們可以透過泛型來為 isFinished 定義型別:
將 isFinished 的型別放入 PropsWithChildren 的泛型參數中即可解決:
import { type PropsWithChildren } from 'react'
// interface TodoProps {
// isFinished: boolean
// }
type TodoProps = PropsWithChildren<{ isFinished: boolean }>
export default function Todo({ isFinished, children }: TodoProps) {
return (
<div className='flex items-center gap-[20px]'>
<input type='checkbox' checked={isFinished} />
{children}
<div className='flex gap-[16px]'>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
)
}
在 import 中加上 type 是 TypeScript 3.8 版本新增的功能。讓我們看一個簡單的例子:
import { Example } from "./example.ts";
此時匯入的 Example 是一個值還是型別呢?當我們明確指定 type 時,可以提高程式碼的可讀性,並有助於加快編譯速度,因為它僅引入型別資訊而不會包含任何運行時的值。在某些情況下,省略 type 可能不會造成問題,但如果使用其他工具進行編譯,省略 type 可能會導致錯誤。
如果想了解更多,可以參考官方網站版本文件,或是 import 和 import type的區別。
在 React 中,使用 map 渲染列表時,React 需要每個項目都有唯一的 key,key 主要是用來在渲染時區分不同的元素,這樣可以更直觀地幫助 React 做出更新。雖然我們在元件中傳入了 key props,但不需要為 key 指定型別,因為 key 是 React 的內建屬性,TypeScript 會自動處理它的型別:
interface FruitProps {
name: string
}
function Fruit({ name }: FruitProps) {
return <li>{name}</li>
}
function App() {
const fruits = [{ name: 'Apple' }, { name: 'Orange' }, { name: 'Banana' }]
return (
<ul>
{fruits.map((fruit) => (
<Fruit key={fruit.name} name={fruit.name} />
))}
</ul>
)
}
export default App