【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法①（Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.）

# はじめに

お疲れ様です。

前回に引き続き、テスト実行時のChakra UI関連のエラー解決方法についてです。

## 【 関連記事 】 今回発生したエラーの解消記事
1. [【Chakra UI v3 × jest】テスト時のDialogContentの型エラーの解消方法（Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.）](https://qiita.com/ritsu21ctws/items/ebf7c9c2e3f082c23ec1)
2. [【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法①（Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.）](https://qiita.com/ritsu21ctws/items/e3ef69cf952c5e4803b2)　**➡ この記事**
3. [【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法②（Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes<HTMLButtonElement>'.）](https://qiita.com/ritsu21ctws/items/e8efbcc831ea0d58f340)
4. [【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法①（Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef<HTMLDivElement>; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.）](https://qiita.com/ritsu21ctws/items/8029cb166116e3ffc31b)
5. [【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法②（Type '{ children: Element; asChild: true; position: string; top: string; insetEnd: string; }' is not assignable to type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes<HTMLButtonElement>'.）](https://qiita.com/ritsu21ctws/items/8940ca3a8eb0bdd79909)
6. [【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法③（Property 'children' does not exist on type 'DialogCloseTriggerProps'.）](https://qiita.com/ritsu21ctws/items/5e217c754e16ead75d2b)

# 問題

`npm run test`実行時に以下のエラーが発生しました。

今回はこのうち、1つ目のエラーの対処方法についてご紹介します。

```bash:エラー内容
FAIL  src/__tests__/App.spec.tsx
  ● Test suite failed to run

    src/components/ui/toaster.tsx:13:8 - error TS2322: Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.
      Property 'children' does not exist on type 'IntrinsicAttributes & ToasterProps'.

    13       <ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
              ~~~~~~~~~~~~~
    src/components/ui/toaster.tsx:22:58 - error TS2322: Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes<HTMLButtonElement>'.
      Property 'style' does not exist on type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes<HTMLButtonElement>'.

    22             {toast.meta?.closable && <Toast.CloseTrigger style={{ width: '25px', height: '25px', color: 'white' }} _hover={{ cursor: 'pointer' }} />}
                                                                ~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        5.162 s
Ran all test suites.
```

## ソースコード

<details><summary>src/App.tsx</summary>

```react:src/App.tsx
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { MdEdit, MdDeleteOutline } from 'react-icons/md';
import { FiPlusCircle } from 'react-icons/fi';
import { Box, Center, Container, Flex, Heading, IconButton, Input, Spinner, Stack, Table } from '@chakra-ui/react';
import { Button } from '@/components/ui/button';
import { Field } from '@/components/ui/field';
import { NumberInputField, NumberInputRoot } from '@/components/ui/number-input';
import { Toaster } from '@/components/ui/toaster';
import { Record } from '@/domain/record';
import { useMessage } from '@/hooks/useMessage';
import { fetchAllRecords, insertRecord, deleteRecord } from '@/utils/supabaseFunctions';
import {
  DialogActionTrigger,
  DialogBody,
  DialogCloseTrigger,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogRoot,
  DialogTitle,
} from '@/components/ui/dialog';

function App() {
  const [records, setRecords] = useState<Record[]>([]);
  const [deleteTargetId, setDeleteTargetId] = useState<string | undefined>(undefined);
  const [isLoading, setIsLoading] = useState(false);
  const [isCreating, setIsCreating] = useState(false);
  const [isDeleting, setIsDeleting] = useState(false);
  const [open, setOpen] = useState(false);
  const [openConfirm, setOpenConfirm] = useState(false);
  const { showMessage } = useMessage();
  const {
    control,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<Record>({
    defaultValues: {
      title: '',
      time: '0',
    },
  });

  useEffect(() => {
    getAllRecords();
  }, []);

  const onClickOpenModal = () => {
    setOpen(true);
  };

  const getAllRecords = () => {
    setIsLoading(true);
    fetchAllRecords()
      .then((data) => {
        setRecords(data);
      })
      .catch(() => {
        showMessage({ title: '一覧の取得に失敗しました', type: 'error' });
      })
      .finally(() => {
        setIsLoading(false);
      });
  };

  const onSubmit = handleSubmit((data: Record) => {
    setIsCreating(true);
    insertRecord(data)
      .then(() => {
        showMessage({ title: '学習記録の登録が完了しました', type: 'success' });
      })
      .catch(() => {
        showMessage({ title: '学習記録の登録に失敗しました', type: 'error' });
      })
      .finally(() => {
        setIsCreating(false);
        reset(); // 登録フォームの初期化
        setOpen(false);
        getAllRecords();
      });
  });

  const onClickDeleteConfirm = (id: string) => {
    setDeleteTargetId(id);
    setOpenConfirm(true);
  };

  const onClickDelete = () => {
    setIsDeleting(true);

    if (typeof deleteTargetId === 'undefined') {
      showMessage({ title: '削除対象のデータが見つかりません', type: 'error' });
      setIsDeleting(false);
      setOpenConfirm(false);
      setDeleteTargetId(undefined);
      return;
    }

    deleteRecord(deleteTargetId)
      .then(() => {
        showMessage({ title: '学習記録の削除が完了しました', type: 'success' });
      })
      .catch(() => {
        showMessage({ title: '学習記録の削除に失敗しました', type: 'error' });
      })
      .finally(() => {
        setIsDeleting(false);
        setOpenConfirm(false);
        setDeleteTargetId(undefined);
        getAllRecords();
      });
  };

  return (
    <>
      <Toaster />

      <Box bg="teal.500" py="4" data-testid="title" color="gray.100">
        <Container maxW="6xl">
          <Flex justify="space-between" align="center">
            <Heading as="h1" textAlign="left">
              学習記録アプリ
            </Heading>
            <IconButton
              aria-label="Search database"
              variant="ghost"
              size="lg"
              color="white"
              _hover={{ bg: 'teal.500', color: 'gray.200' }}
              onClick={onClickOpenModal}
            >
              <FiPlusCircle />
            </IconButton>
          </Flex>
        </Container>
      </Box>

      {isLoading ? (
        <Center h="100vh">
          <Spinner />
        </Center>
      ) : (
        <Container maxW="6xl">
          <Table.Root size="md" variant="line" my={10} interactive>
            <Table.Header>
              <Table.Row>
                <Table.ColumnHeader>タイトル</Table.ColumnHeader>
                <Table.ColumnHeader>時間</Table.ColumnHeader>
                <Table.ColumnHeader textAlign="end"></Table.ColumnHeader>
              </Table.Row>
            </Table.Header>
            <Table.Body>
              {records.map((record) => (
                <Table.Row key={record.id}>
                  <Table.Cell>{record.title}</Table.Cell>
                  <Table.Cell>{record.time}</Table.Cell>
                  <Table.Cell textAlign="end">
                    <Button colorPalette="blue" variant="outline" mr="4">
                      <MdEdit />
                      編集
                    </Button>
                    <Button colorPalette="red" variant="outline" onClick={() => onClickDeleteConfirm(record.id)}>
                      <MdDeleteOutline />
                      削除
                    </Button>
                  </Table.Cell>
                </Table.Row>
              ))}
            </Table.Body>
          </Table.Root>
        </Container>
      )}

      <DialogRoot lazyMount open={open} onOpenChange={(e) => setOpen(e.open)} motionPreset="slide-in-bottom" trapFocus={false}>
        <DialogContent>
          <DialogCloseTrigger />
          <DialogHeader>
            <DialogTitle>学習記録登録</DialogTitle>
          </DialogHeader>
          <form onSubmit={onSubmit}>
            <DialogBody>
              <Stack gap={4}>
                <Field label="学習内容" invalid={!!errors.title} errorText={errors.title?.message}>
                  <Controller
                    name="title"
                    control={control}
                    rules={{
                      required: '内容の入力は必須です',
                    }}
                    render={({ field }) => <Input {...field} />}
                  />
                </Field>
                <Field label="学習時間" invalid={!!errors.time} errorText={errors.time?.message}>
                  <Controller
                    name="time"
                    control={control}
                    rules={{
                      required: '時間の入力は必須です',
                      min: { value: 0, message: '時間は0以上である必要があります' },
                    }}
                    render={({ field }) => (
                      <NumberInputRoot width="100%" name={field.name} value={field.value} onValueChange={({ value }) => field.onChange(value)}>
                        <NumberInputField />
                      </NumberInputRoot>
                    )}
                  />
                </Field>
              </Stack>
            </DialogBody>
            <DialogFooter mb="2">
              <DialogActionTrigger asChild>
                <Button variant="outline">キャンセル</Button>
              </DialogActionTrigger>
              <Button colorPalette="teal" loading={isCreating} type="submit">
                登録
              </Button>
            </DialogFooter>
          </form>
        </DialogContent>
      </DialogRoot>

      <DialogRoot
        role="alertdialog"
        lazyMount
        open={openConfirm}
        onOpenChange={(e) => setOpenConfirm(e.open)}
        motionPreset="slide-in-bottom"
        trapFocus={false}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>削除の確認</DialogTitle>
          </DialogHeader>
          <DialogBody>
            <p>削除したデータは戻せません。削除してもよろしいですか？</p>
          </DialogBody>
          <DialogFooter mb="2">
            <DialogActionTrigger asChild>
              <Button variant="outline">キャンセル</Button>
            </DialogActionTrigger>
            <Button colorPalette="red" loading={isDeleting} onClick={onClickDelete}>
              削除
            </Button>
          </DialogFooter>
          <DialogCloseTrigger />
        </DialogContent>
      </DialogRoot>
    </>
  );
}

export default App;
```

</details>

<details><summary>src/__tests__/App.spec.tsx</summary>

```react:src/__tests__/App.spec.tsx
import App from '../App';
import { render, screen } from '@testing-library/react';
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';

describe('App', () => {
  test('タイトルがあること', async () => {
    render(
      <ChakraProvider value={defaultSystem}>
        <App />
      </ChakraProvider>
    );
    const title = screen.getByTestId('title');
    expect(title).toBeInTheDocument();
  });
});
```

</details>

# 解決方法

`src/components/ui/toaster.tsx`を修正します。

```diff_tsx:src/components/ui/toaster.tsx
'use client';

- import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast, createToaster } from '@chakra-ui/react';
+ import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast, createToaster, type ToasterProps } from '@chakra-ui/react'; // ToasterProps型をインポート

export const toaster = createToaster({
  placement: 'top',
  pauseOnPageIdle: true,
});

+ // toasterPropsを型「ToasterProps」として定義
+ const toasterProps: ToasterProps = {
+   toaster: toaster,
+   insetInline: { mdDown: '4' },
+   children: (toast) => (
+     <Toast.Root width={{ md: 'sm' }}>
+       {toast.type === 'loading' ? <Spinner size="sm" color="blue.solid" /> : <Toast.Indicator />}
+       <Stack gap="1" flex="1" maxWidth="100%">
+         {toast.title && <Toast.Title>{toast.title}</Toast.Title>}
+         {toast.description && <Toast.Description>{toast.description}</Toast.Description>}
+       </Stack>
+       {toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
+       {toast.meta?.closable && <Toast.CloseTrigger style={{ width: '25px', height: '25px', color: 'white' }} _hover={{ cursor: 'pointer' }} />}
+     </Toast.Root>
+   ),
+ };

export const Toaster = () => {
  return (
    <Portal>
-     <ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
-       {(toast) => (
-         <Toast.Root width={{ md: 'sm' }}>
-           {toast.type === 'loading' ? <Spinner size="sm" color="blue.solid" /> : <Toast.Indicator />}
-           <Stack gap="1" flex="1" maxWidth="100%">
-             {toast.title && <Toast.Title>{toast.title}</Toast.Title>}
-             {toast.description && <Toast.Description>{toast.description}</Toast.Description>}
-           </Stack>
-           {toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
-           {toast.meta?.closable && <Toast.CloseTrigger style={{ width: '25px', height: '25px', color: 'white' }} _hover={{ cursor: 'pointer' }} />}
-         </Toast.Root>
-       )}
-     </ChakraToaster>
+     <ChakraToaster {...toasterProps} />  {/* toasterPropsに置き換え */}
    </Portal>
  );
};
```

# おわりに

上記の対応で`Toast`のエラーのうちの1つが解消されました。

ただ、対処療法的なやり方のため、よりよい解決方法をご存知の方がいらっしゃればぜひご教示いただきたいです。

もう一つのエラーについては別記事で解決していこうと思います。

**➡ 記事を書きました。**

https://qiita.com/ritsu21ctws/items/e8efbcc831ea0d58f340

# 参考

https://www.chakra-ui.com/docs/components/toast