MUI의 AutoComplete를 활용하는, 검색 컴포넌트를 구현할 일이 생겼다. 그래서 나는 Foo를 검색하는 컴포넌트를 뚝딱 만들었다.
/* 설명을 위해 간소화한 버전입니다. */
interface SearchProps {
initialOption?: Frontend.UseSearchOption;
initialOptionList?: Frontend.UseSearchOption[];
}
function Search(props: SearchProps) {
const { initialOption, initialOptionList } = props;
const [optionList, setOptionList] = useState<Frontend.UseSearchOption[]>(
initialOptionList ? initialOptionList : [],
);
// ...
return (
<MUI.Autocomplete
options={optionList}
defaultValue={initialOption ? initialOption : undefined}
renderInput={(params) => {
const { InputProps, id, inputProps, disabled } = params;
return (
<MUI.TextField .../>
);
}}
/>
);
}
<Search
{/*렌더링 시 미리 선택되어 있어야 하는 값을 설정합니다.*/}
initialOption={
{ id: 'foo', name: 'foo' },
}
{/*렌더링 시 선택 가능한 옵션 값들을 설정합니다.*/}
initialOptionList={[
{ id: 'foo', name: 'foo' },
{ id: 'bar', name: 'bar' },
{ id: 'baz', name: 'baz' },
]}
/>
사실 구현하는 것 자체는 매우 쉬웠다. 그런데 이내 옵션을 여러개 선택할 수 있게 해달라는 요구사항이 나왔다.
그래서 나는 isMultiple 이라는 속성을 하나 추가했다.
/* 설명을 위해 간소화한 버전입니다. */
interface SearchProps {
// ...
isMultiple?: boolean;
}
function Search(props: SearchProps) {
const { initialOption, initialOptionList, isMultiple } = props;
// ...
return (
<MUI.Autocomplete
multiple={isMultiple}
{/* ... */}
/>
);
}
이제 해당 컴포넌트는 여러 옵션을 선택할 수 있게 되었다.
<Search
isMultiple
initialOption={[
{ id: 'foo', name: 'foo' },
{ id: 'bar', name: 'bar' },
]}
{/*렌더링 시 선택 가능한 옵션 값들을 설정합니다.*/}
initialOptionList={[
{ id: 'foo', name: 'foo' },
{ id: 'bar', name: 'bar' },
{ id: 'baz', name: 'baz' },
]}
/>
여기까지 동작의 문제는 없다. 다만, 조금 더 안정성을 기하기 위해 타입 차원에서 다음과 같은 요구사항을 검증하고 싶었다.
isMultple 속성이 true 라면, 반드시 initialOption 은 배열이어야 한다.isMultple 속성이 false 거나 undefined 라면, initialOption 은 배열이 아니어야 한다.즉, 다음과 같은 경우, 타입 에러가 발생했으면 좋겠다는 뜻이다.
<Search
isMultiple
initialOption={
{ id: 'foo', name: 'foo' },
}
/>
<Search
initialOption={[
{ id: 'foo', name: 'foo' },
{ id: 'bar', name: 'bar' },
]}
/>
나는 어떻게 이것을 구현할 수 있을지 이리저리 고민을 하다, MUI의 타입 정의를 한번 살펴보기로 했다.
export interface UseAutocompleteProps<
T,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
> {
defaultValue?: AutocompleteValue<T, Multiple, DisableClearable, FreeSolo>;
}
export type AutocompleteValue<T, Multiple, DisableClearable, FreeSolo> = Multiple extends true
? Array<T | AutocompleteFreeSoloValueMapping<FreeSolo>>
: DisableClearable extends true
? NonNullable<T | AutocompleteFreeSoloValueMapping<FreeSolo>>
: T | null | AutocompleteFreeSoloValueMapping<FreeSolo>;
눈여겨 볼 것은 Multiple 제네릭과 Multiple extends true ? Array<T...> : ... 부분이다.
즉 Multiple 타입이 true 이냐 아니냐에 따라 defaultValue 가 배열인지 아닌지 타입 차원에서 검증할 수 있다.
이 부분을 차용해서 나도 가볍게 유틸리티 타입을 만들었다.
type SearchOptionValue<T, Multiple> = Multiple extends true ? Array<T> : T;
그리고 컴포넌트 속성 타입 정의를 조금 손봐준다.
interface SearchProps<T, Multiple> {
initialOption?: SearchOptionValue<T, Multiple>;
initialOptionList?: T[];
isMultiple?: Multiple;
}
function Search<Multiple>(
props: SearchProps<Frontend.UseCharacterSearchOption, Multiple>,
) {
// ...
}
이제 실제 사용을 해보자.

[isMultiple 이 undefined 인 경우, initialOption 이 배열이 아니라면 정상]

[isMultiple 이 true 인 경우, initialOption 이 배열이 아니라면 타입 에러 발생 (에러 메세지는.. 조금 똥이긴 하다)]

[isMultiple 이 true 인 경우, initialOption 이 배열이라면 정상]

[isMultiple 이 undefined 인 경우, initialOption 이 배열이라면 타입 에러 발생]
MUI.AutoComplete 와 함께 쓰려면 컴포넌트의 타입을 쪼끔 손봐줘야 한다.

[AutoComplete 와 연동하는데 타입 에러가 발생한다.]
function Search<Multiple extends boolean | undefined = false>(
props: SearchProps<Frontend.UseCharacterSearchOption, Multiple>,
) {
// ...
}
컴포넌트 제네릭 타입 Multiple 에 extends boolean | undefined = false 를 추가해주면 해결!