【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法③（Property 'children' does not exist on type 'DialogCloseTriggerProps'.）

# はじめに

お疲れ様です。

前回に引き続き、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`実行時に以下のエラーが発生しました。

今回は3つ目のエラーの解消方法です。

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

    src/components/ui/dialog.tsx:19:10 - error TS2322: Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef<HTMLDivElement>; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.

    19         <ChakraDialog.Content ref={ref} {...rest} asChild={false}>
                ~~~~~~~~~~~~~~~~~~~~
    src/components/ui/dialog.tsx:29:6 - error TS2322: Type '{ children: Element; asChild: true; position: string; top: string; insetEnd: string; }' is not assignable to type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes<HTMLButtonElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes<HTMLButtonElement>'.

    29     <ChakraDialog.CloseTrigger position="absolute" top="2" insetEnd="2" {...props} asChild>
            ~~~~~~~~~~~~~~~~~~~~~~~~~
    src/components/ui/dialog.tsx:31:16 - error TS2339: Property 'children' does not exist on type 'DialogCloseTriggerProps'.

    31         {props.children}
                      ~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        3.808 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/dialog.tsx`を修正します。

```diff_tsx:src/components/ui/dialog.tsx
import { Dialog as ChakraDialog, Portal, type DialogCloseTriggerProps } from '@chakra-ui/react';
import { CloseButton } from './close-button';
import * as React from 'react';

interface DialogContentProps extends ChakraDialog.ContentProps {
  portalled?: boolean;
  portalRef?: React.RefObject<HTMLElement>;
  backdrop?: boolean;
  children?: React.ReactNode;
  asChild?: boolean;
  ref?: React.ForwardedRef<HTMLDivElement>;
}

+ // DialogCloseTriggerPropsを継承した新しいインターフェースを定義し、childrenを追加
+ interface NewDialogCloseTriggerProps extends DialogCloseTriggerProps {
+   children?: React.ReactNode;
+ }

export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(function DialogContent(props, ref) {
  const { children, portalled = true, portalRef, backdrop = true, ...rest } = props;
  const chakraDialogContentProps: DialogContentProps = {
    ref: ref,
    asChild: false,
    children: children,
  };

  return (
    <Portal disabled={!portalled} container={portalRef}>
      {backdrop && <ChakraDialog.Backdrop />}
      <ChakraDialog.Positioner>
        <ChakraDialog.Content {...rest} {...chakraDialogContentProps} />
      </ChakraDialog.Positioner>
    </Portal>
  );
});

- export const DialogCloseTrigger = React.forwardRef<HTMLButtonElement, ChakraDialog.CloseTriggerProps>(function DialogCloseTrigger(props, ref) {
+ export const DialogCloseTrigger = React.forwardRef<HTMLButtonElement, NewDialogCloseTriggerProps>(function DialogCloseTrigger(props, ref) { // NewDialogCloseTriggerPropsに置き換え
  const chakraDialogCloseTriggerProps: DialogCloseTriggerProps = {
    asChild: true,
    position: 'absolute',
    top: '2',
    insetEnd: '2',
    children: (
      <CloseButton size="sm" ref={ref}>
        {props.children}
      </CloseButton>
    ),
  };

  return <ChakraDialog.CloseTrigger {...chakraDialogCloseTriggerProps} {...props} />;
});

export const DialogRoot = ChakraDialog.Root;
export const DialogFooter = ChakraDialog.Footer;
export const DialogHeader = ChakraDialog.Header;
export const DialogBody = ChakraDialog.Body;
export const DialogBackdrop = ChakraDialog.Backdrop;
export const DialogTitle = ChakraDialog.Title;
export const DialogDescription = ChakraDialog.Description;
export const DialogTrigger = ChakraDialog.Trigger;
export const DialogActionTrigger = ChakraDialog.ActionTrigger;
```

# おわりに

これまでの対応によって、自分の環境では全てのエラーが解消され、テストが通るようになりました。
特に`Dialog`、`Toast`によるエラーが多かったですね。

もし他によい解決方法をご存知の方がいらっしゃれば、ご教示いただけると幸いです。

# 参考

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