【Chakra UI v3 × jest】テスト時のSelectの型エラーの解消方法①（Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'. 他）

# はじめに

お疲れ様です、りつです。

今回はテスト実行時に発生したChakra UI v3`Select`の型エラーの解消方法について記事にまとめました。

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

# 問題

テスト実施時に、以下のようなエラーが発生しました。
今回は以下の4つのエラーの解消方法を共有いたします。

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

    src/components/pages/Register.tsx:101:28 - error TS2559: Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'.

    101                           <SelectTrigger>
                                   ~~~~~~~~~~~~~
    src/components/pages/Register.tsx:102:46 - error TS2322: Type '{ placeholder: string; }' is not assignable to type 'IntrinsicAttributes & SelectValueTextProps & RefAttributes<HTMLSpanElement>'.
      Property 'placeholder' does not exist on type 'IntrinsicAttributes & SelectValueTextProps & RefAttributes<HTMLSpanElement>'.

    102                             <SelectValueText placeholder="Select Option" />
                                                     ~~~~~~~~~~~
    src/components/pages/Register.tsx:104:28 - error TS2559: Type '{ children: any; }' has no properties in common with type 'IntrinsicAttributes & SelectContentProps & RefAttributes<HTMLDivElement>'.

    104                           <SelectContent>
                                   ~~~~~~~~~~~~~
    src/components/pages/Register.tsx:106:32 - error TS2322: Type '{ children: any; item: any; key: any; }' is not assignable to type 'IntrinsicAttributes & SelectItemProps & RefAttributes<HTMLDivElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectItemProps & RefAttributes<HTMLDivElement>'.

    106                               <SelectItem item={skill} key={skill.value}>
                                       ~~~~~~~~~~

Test Suites: 1 failed, 1 total
```

<details><summary>参考ファイル</summary>

```src/App.tsx
import { Router } from '@/router/Router';
import { Toaster } from '@/components/ui/toaster';

function App() {
  return (
    <>
      <Router />
      <Toaster />
    </>
  );
}

export default App;
```

```react:src/components/pages/Register.tsx
import React, { memo, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { Controller, useForm } from 'react-hook-form';
import { Button, Card, Center, createListCollection, Heading, Textarea, Input, Stack, ListCollection } from '@chakra-ui/react';
import { Field } from '@/components/ui/field';
import { SelectContent, SelectItem, SelectRoot, SelectTrigger, SelectValueText } from '@/components/ui/select';
import { FormData } from '@/domain/formData';
import { useMessage } from '@/hooks/useMessage';
import { fetchSkills, insertUser } from '@/utils/supabaseFunctions';

export const Register: React.FC = memo(() => {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>();
  const navigate = useNavigate();
  const { showMessage } = useMessage();

  const [skills, setSkills] = useState<ListCollection<{ label: string; value: string }> | null>(null);

  useEffect(() => {
    fetchSkills()
      .then((data) => {
        setSkills(createListCollection({ items: data.map((skill) => ({ label: skill.name, value: skill.id.toString() })) }));
      })
      .catch(() => {
        showMessage({ title: 'データの取得に失敗しました', type: 'error' });
      });
  }, []);

  const onSubmit = handleSubmit((data: FormData) => {
    insertUser(data)
      .then(() => {
        showMessage({ title: '登録が完了しました', type: 'success' });
        navigate('/');
      })
      .catch(() => {
        showMessage({ title: '登録に失敗しました', type: 'error' });
      });
  });

  return (
    <>
      <Center my="5">
        <Stack>
          <Heading as="h1" mb="2" textAlign="center">
            名刺新規登録
          </Heading>
          <Card.Root width="340px" variant="elevated">
            <form onSubmit={onSubmit}>
              <Card.Body>
                <Stack gap="4" w="full">
                  <Field label="好きな英単語 *" invalid={!!errors.user_id} errorText={errors.user_id?.message}>
                    <Controller
                      name="user_id"
                      control={control}
                      rules={{
                        required: '好きな英単語の入力は必須です',
                        pattern: { value: /^[a-zA-Z]+$/, message: '好きな英単語は半角英字で入力してください。' },
                      }}
                      render={({ field }) => <Input {...field} placeholder="coffee" />}
                    />
                  </Field>
                  <Field label="名前 *" invalid={!!errors.name} errorText={errors.name?.message}>
                    <Controller
                      name="name"
                      control={control}
                      rules={{
                        required: '名前の入力は必須です',
                      }}
                      render={({ field }) => <Input {...field} />}
                    />
                  </Field>
                  <Field label="自己紹介 *" invalid={!!errors.description} errorText={errors.description?.message}>
                    <Controller
                      name="description"
                      control={control}
                      rules={{
                        required: '自己紹介の入力は必須です',
                      }}
                      render={({ field }) => <Textarea {...field} placeholder="<h1>HTMLタグも使えます</h1>" />}
                    />
                  </Field>
                  <Field label="好きな技術" invalid={!!errors.skills} errorText={errors.skills?.message}>
                    <Controller
                      name="skills"
                      control={control}
                      rules={{
                        required: '好きな技術の入力は必須です',
                      }}
                      render={({ field }) => (
                        <SelectRoot
                          name={field.name}
                          value={field.value}
                          onValueChange={({ value }) => field.onChange(value)}
                          onInteractOutside={() => field.onBlur()}
                          multiple
                          collection={skills || createListCollection({ items: [] })}
                        >
                          <SelectTrigger>
                            <SelectValueText placeholder="Select Option" />
                          </SelectTrigger>
                          <SelectContent>
                            {skills?.items.map((skill) => (
                              <SelectItem item={skill} key={skill.value}>
                                {skill.label}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </SelectRoot>
                      )}
                    />
                  </Field>
                  <Field label="GitHub ID">
                    <Controller name="github_id" control={control} render={({ field }) => <Input {...field} />} />
                  </Field>
                  <Field label="Qiita ID">
                    <Controller name="qiita_id" control={control} render={({ field }) => <Input {...field} />} />
                  </Field>
                  <Field label="X ID">
                    <Controller name="x_id" control={control} render={({ field }) => <Input {...field} />} />
                  </Field>
                  *は必須項目です
                </Stack>
              </Card.Body>
              <Card.Footer justifyContent="flex-end">
                <Button variant="solid" type="submit" aria-label="Submit" colorPalette="cyan" w="full">
                  登録
                </Button>
              </Card.Footer>
            </form>
          </Card.Root>
        </Stack>
      </Center>
    </>
  );
});
```

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

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

</details>

# 解決方法

`src/components/ui/select.tsx`の内容を修正します。
なお、コメントの番号は、何番目のエラーに対応するかを表しています。

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

- import type { CollectionItem } from '@chakra-ui/react';
+ import type { CollectionItem, SelectItemProps } from '@chakra-ui/react'; // 4. SelectItemProps型をインポート
import { Select as ChakraSelect, Portal } from '@chakra-ui/react';
import { CloseButton } from './close-button';
import * as React from 'react';

interface SelectTriggerProps extends ChakraSelect.ControlProps {
  clearable?: boolean;
+ children?: React.ReactNode; // 1. children を追加
}

export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(function SelectTrigger(props, ref) {
  const { children, clearable, ...rest } = props;
  return (
    <ChakraSelect.Control {...rest}>
      <ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
      <ChakraSelect.IndicatorGroup>
        {clearable && <SelectClearTrigger />}
        <ChakraSelect.Indicator />
      </ChakraSelect.IndicatorGroup>
    </ChakraSelect.Control>
  );
});

const SelectClearTrigger = React.forwardRef<HTMLButtonElement, ChakraSelect.ClearTriggerProps>(function SelectClearTrigger(props, ref) {
  return (
    <ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
      <CloseButton size="xs" variant="plain" focusVisibleRing="inside" focusRingWidth="2px" pointerEvents="auto" />
    </ChakraSelect.ClearTrigger>
  );
});

interface SelectContentProps extends ChakraSelect.ContentProps {
  portalled?: boolean;
  portalRef?: React.RefObject<HTMLElement>;
+ children?: React.ReactNode; // 3. children を追加
}

export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(function SelectContent(props, ref) {
  const { portalled = true, portalRef, ...rest } = props;
  return (
    <Portal disabled={!portalled} container={portalRef}>
      <ChakraSelect.Positioner>
        <ChakraSelect.Content {...rest} ref={ref} />
      </ChakraSelect.Positioner>
    </Portal>
  );
});

+ // 4. SelectItemPropsを継承した新しいインターフェースを定義し、必要な項目を追加
+ interface NewSelectItemProps extends SelectItemProps {
+  children?: React.ReactNode;
+  item: CollectionItem;
+  key?: string;
+ }

- export const SelectItem = React.forwardRef<HTMLDivElement, ChakraSelect.ItemProps>(function SelectItem(props, ref) {
+ export const SelectItem = React.forwardRef<HTMLDivElement, NewSelectItemProps>(function SelectItem(props, ref) { // 4. NewSelectItemPropsに置き換え

  const { item, children, ...rest } = props;
  return (
    <ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
      {children}
      <ChakraSelect.ItemIndicator />
    </ChakraSelect.Item>
  );
});

interface SelectValueTextProps extends Omit<ChakraSelect.ValueTextProps, 'children'> {
  children?(items: CollectionItem[]): React.ReactNode;
+ placeholder: string; // 2. placeholder を追加
}

export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(function SelectValueText(props, ref) {
  const { children, ...rest } = props;
  return (
    <ChakraSelect.ValueText {...rest} ref={ref}>
      <ChakraSelect.Context>
        {(select) => {
          const items = select.selectedItems;
          if (items.length === 0) return props.placeholder;
          if (children) return children(items);
          if (items.length === 1) return select.collection.stringifyItem(items[0]);
          return `${items.length} selected`;
        }}
      </ChakraSelect.Context>
    </ChakraSelect.ValueText>
  );
});

export const SelectRoot = React.forwardRef<HTMLDivElement, ChakraSelect.RootProps>(function SelectRoot(props, ref) {
  return (
    <ChakraSelect.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }}>
      {props.asChild ? (
        props.children
      ) : (
        <>
          <ChakraSelect.HiddenSelect />
          {props.children}
        </>
      )}
    </ChakraSelect.Root>
  );
}) as ChakraSelect.RootComponent;

interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
  label: React.ReactNode;
}

export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(function SelectItemGroup(props, ref) {
  const { children, label, ...rest } = props;
  return (
    <ChakraSelect.ItemGroup {...rest} ref={ref}>
      <ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
      {children}
    </ChakraSelect.ItemGroup>
  );
});

export const SelectLabel = ChakraSelect.Label;
export const SelectItemText = ChakraSelect.ItemText;
```

# おわりに

上記の修正により、該当エラーは解消されましたが、別のエラーが発生しました。
こちらについては別の記事で解消していきます。

https://qiita.com/ritsu21ctws/items/0657d009d857d7988685

# 参考

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