컨디셔널 타입을 이용해 컴포넌트의 속성을 자동으로 추론하게 해보자!

Einere·2024년 1월 21일
1

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 이 배열이 아니라면 정상
[isMultipleundefined 인 경우, initialOption 이 배열이 아니라면 정상]

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

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

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

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

isMultiple 이 undefined 인 경우, initialOption 이 배열이라면 타입 에러 발생

[isMultipleundefined 인 경우, initialOption 이 배열이라면 타입 에러 발생]

MUI.AutoComplete 와 함께 쓰려면 컴포넌트의 타입을 쪼끔 손봐줘야 한다.

AutoComplete 와 연동하는데 타입 에러가 발생한다.

[AutoComplete 와 연동하는데 타입 에러가 발생한다.]

function Search<Multiple extends boolean | undefined = false>(
  props: SearchProps<Frontend.UseCharacterSearchOption, Multiple>,
) {
  // ...
}

컴포넌트 제네릭 타입 Multipleextends boolean | undefined = false 를 추가해주면 해결!

profile
웹 기술로 문제를 해결하는, 지속가능한 엔지니어를 지향합니다.

0개의 댓글