iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
自我挑戰組

React 開發者的 TypeScript 探索之旅系列 第 13

【 Day 13 】為 React props 定義型別

  • 分享至 

  • xImage
  •  

本系列文章 GitHub

基本定義型別

目前我們的 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>
  )
}

https://ithelp.ithome.com.tw/upload/images/20240923/20169025qwtt357XND.png


Custom Types & Interface

雖然我們上面的做法並不會有問題,但是當傳入的 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>
  )
}

children

相信 React 開發者們對於 children 這個 props 肯定不陌生,當元件包覆了其他內容,這個內容就會是該元件的 children,首先,我們先到 App.tsxcontent 改為 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.tsxchildren 定義型別,你認為 children 的型別是什麼呢?因為我們的內容是 Learn typeScript,所以是 string 嗎?
我們都知道在 React 元件中 return 的內容會是 JSX,所以我們不能直接定義為 string
在這邊要提到一個特別的型別,它叫做 ReactNodeReactNode 是 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,這與我們先前指定的型別是一致的:
https://ithelp.ithome.com.tw/upload/images/20240923/20169025dlDBiITrmz.png

程式碼報錯的原因是因為我們尚未為 isFinished 指定型別。將滑鼠移到 PropsWithChildren 上時,IDE 會提示我們:PropsWithChildren 是一個泛型工具,允許我們傳遞其他型別作為參數。接下來,我們可以透過泛型來為 isFinished 定義型別:
https://ithelp.ithome.com.tw/upload/images/20240923/20169025lWdXzTl7hV.png

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 ReactNode } from 'react'

import 中加上 type 是 TypeScript 3.8 版本新增的功能。讓我們看一個簡單的例子:

import { Example } from "./example.ts";

此時匯入的 Example 是一個值還是型別呢?當我們明確指定 type 時,可以提高程式碼的可讀性,並有助於加快編譯速度,因為它僅引入型別資訊而不會包含任何運行時的值。在某些情況下,省略 type 可能不會造成問題,但如果使用其他工具進行編譯,省略 type 可能會導致錯誤。

如果想了解更多,可以參考官方網站版本文件,或是 import 和 import type的區別

【 補充二 】Key props

在 React 中,使用 map 渲染列表時,React 需要每個項目都有唯一的 keykey 主要是用來在渲染時區分不同的元素,這樣可以更直觀地幫助 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

上一篇
【 Day 12 】TypeScript - Strict Mode
下一篇
【 Day 14 】props in arrow function component
系列文
React 開發者的 TypeScript 探索之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言