Cara Memberi Type Prop children React di TypeScript

5 menit baca.
Tags: ReactTypeScript

Untuk memberi type prop children React di TypeScript, deklarasikan sendiri sebagai React.ReactNode. Sejak React 18, komponen tidak otomatis mendapat children, jadi kamu harus menulisnya di type props.

Jawaban cepat

import type { ReactNode } from 'react'

type CardProps = {
  title: string
  children: ReactNode
}

function Card({ title, children }: CardProps) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  )
}

ReactNode mencakup semua yang bisa dirender React: elemen JSX, string, number, null, undefined, boolean, dan array dari semua itu.

<Card title="Mixed">
  text {1} {null} {false} {[<b key="1" />]} <i />
</Card>

Semua itu lolos compile.

Pakai children?: ReactNode kalau komponennya tetap jalan tanpa isi, misalnya <Panel />.

React.FC tidak lagi menambahkan children

Tutorial lama menulis seperti ini:

const Layout: React.FC = ({ children }) => <main>{children}</main>

Dengan type React 18 atau 19, kode ini gagal:

error TS2339: Property 'children' does not exist on type '{}'.

Dulu React.FC menambahkan children ke props semua komponen, termasuk yang tidak pernah merendernya. Type React 18 menghapus perilaku itu. Tambahkan secara eksplisit:

const Layout: React.FC<{ children?: ReactNode }> = ({ children }) => (
  <main>{children}</main>
)

Atau lewati React.FC dan beri type langsung ke parameter props, seperti contoh Card di atas. Hasilnya sama.

PropsWithChildren untuk versi singkat

React menyediakan helper yang menambahkan children?: ReactNode opsional ke type props apa pun:

import type { PropsWithChildren } from 'react'

function Box({ padding, children }: PropsWithChildren<{ padding: number }>) {
  return <div style={{ padding }}>{children}</div>
}

Di sini children opsional, jadi <Box padding={8} /> juga lolos compile. Kalau komponennya wajib punya children, tulis children: ReactNode sendiri.

Kenapa bukan JSX.Element?

JSX.Element (atau React.JSX.Element di type React 19) artinya tepat satu elemen JSX. Teks akan gagal:

type Props = { children: React.JSX.Element }

<Wrapper>plain text</Wrapper>
error TS2747: 'Wrapper' components don't accept text as child elements. Text in JSX has the type 'string', but the expected type of 'children' is 'Element'.

Dua elemen juga gagal:

error TS2746: This JSX tag's 'children' prop expects a single child of type 'Element', but multiple children were provided.

Di @types/react 19, namespace global JSX.Element sudah tidak ada dan menghasilkan Cannot find namespace 'JSX'. Pakai React.JSX.Element kalau memang mau tepat satu elemen. Untuk wrapper biasa, ReactNode adalah type yang tepat.

Kalau butuh tepat satu elemen

Beberapa komponen meng-clone child-nya untuk menambahkan props, misalnya tooltip yang mengisi title. Beri type child sebagai ReactElement dengan props yang akan kamu set:

import { cloneElement, type ReactElement } from 'react'

type TooltipProps = {
  text: string
  children: ReactElement<{ title?: string }>
}

function Tooltip({ text, children }: TooltipProps) {
  return cloneElement(children, { title: text })
}

<Tooltip text="Save changes">
  <button>Save</button>
</Tooltip>

Children berupa function

Untuk pola render prop, beri type children sebagai function yang mengembalikan ReactNode:

type MouseProps = {
  children: (pos: { x: number; y: number }) => ReactNode
}

function Mouse({ children }: MouseProps) {
  const pos = { x: 0, y: 0 } // track the real position here
  return <div>{children(pos)}</div>
}

<Mouse>{({ x, y }) => <p>{x}, {y}</p>}</Mouse>

TypeScript menyimpulkan x dan y di dalam callback dari type props-nya.

Children tidak bisa dibatasi ke satu jenis komponen

Percobaan yang sering muncul adalah membatasi <Tabs> supaya hanya menerima <Tab>:

type TabsProps = {
  children: ReactElement<TabProps> | ReactElement<TabProps>[]
}

<Tabs>
  <div>not a tab</div>
</Tabs>

Kode ini lolos compile tanpa error. Setiap ekspresi JSX punya type yang sama, React.JSX.Element, apa pun komponen yang dirender, jadi TypeScript tidak bisa membedakan <div> dari <Tab>. Kalau butuh jaminan itu, cek di runtime (child.type === Tab), atau terima datanya lewat prop, misalnya tabs={[{ label: 'One', content: ... }]}.

Kesalahan yang sering terjadi

Memakai React.FC dan berharap ada children. Sejak React 18 sudah tidak implisit. Deklarasikan sendiri.

Memberi type children sebagai JSX.Element. Teks dan beberapa child ditolak. Pakai ReactNode.

Memberi type children sebagai string. Akibatnya <Button><Icon /> Save</Button> gagal. Pakai ReactNode kecuali komponennya memang hanya menerima teks.

Merender object biasa. ReactNode tidak mencakup object, jadi <Card>{{ a: 1 }}</Card> jadi type error. Ini mencegah crash "Objects are not valid as a React child" di runtime.

Kesimpulan

Beri type children sebagai React.ReactNode, ditulis eksplisit di type props, dan buat opsional hanya kalau komponennya tetap jalan tanpa isi. Pakai ReactElement kalau meng-clone satu child, type function untuk render prop, dan PropsWithChildren kalau children opsional tidak masalah.