はじめに
お疲れ様です、りつです。
今回はテスト実行時に発生したChakra UI v3Select
の型エラーの解消方法について記事にまとめました。
問題
テスト実施時に、以下のようなエラーが発生しました。
今回は以下の4つのエラーの解消方法を共有いたします。
エラー内容
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
参考ファイル
src/App.tsx
import { Router } from '@/router/Router';
import { Toaster } from '@/components/ui/toaster';
function App() {
return (
<>
<Router />
<Toaster />
</>
);
}
export default App;
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();
});
});
解決方法
src/components/ui/select.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;
おわりに
上記の修正により、該当エラーは解消されましたが、別のエラーが発生しました。
こちらについては別の記事で解消していきます。
参考