`string` 형식의 인수는 `...` 형식의 매개 변수에 할당될 수 없습니다. | TypeScript

Bori·2023년 5월 17일
6

어쨌든 공부

목록 보기
17/40

매개 변수에 할당될 수 없습니다. 왜죠?

다정한 타입스크립트는 저에게 자주 메시지를 보내는데요.이번에는 아래와 같은 메시지를 받았습니다.

오류 발생 상황 공유

회원가입 페이지의 회원가입 약관 동의를 받는 부분을 구현하고 있습니다.
회원가입에 필요한 사용자의 입력값은 다음과 같이 타입을 정의하고, useForm에 해당 타입을 적용하여 사용했습니다.

// 타입 정의
export interface RegisterSchema {
  email: string;
  username: string;
  password: string;
  passwordCheck: string;
  imgUrl: string;
  termsAgreement: {
    service: boolean;
    privacy: boolean;
    marketing: boolean;
  };
}

// 다음과 같이 타입 적용
const { register, setValue } = useForm<RegisterSchema>();

약관 항목은 다음과 같이 상수로 관리하고 있습니다.

const TERMS_AND_CONDITIONS = [
  { id: 'service', title: '서비스 이용약관', required: true },
  { id: 'privacy', title: '개인정보 수집 및 이용 동의', required: true },
  { id: 'marketing', title: '마케팅 활용 동의', required: false },
];

TERMS_AND_CONDITIONS의 값을 이용하기 위해 map을 이용하여 구현합니다.

<CheckboxList>
  {TERMS_AND_CONDITIONS.map((term) => {
    const { id, title, required } = term;
    const fieldName = `termsAgreement.${id}`;
    return (
      <li key={`terms-and-conditions-${id}`}>
        <CheckboxInput
          id={id}
          type="checkbox"
          checked={agreedToTerms[id]}
          {...register(fieldName, { // 타입스크립트 오류 발생 위치
            required: !!required,
            onChange: handleOnToggleCheckbox,
          })}
        />
      </li>
    );
  })}
</CheckboxList>

이렇게 하면 타입스크립트 오류가 발생합니다. 어디에서 발생하냐면 저기 registerfieldName이 적용된 곳에서요. 주석 보이시죠? 하하
오류는 글 상단에서 본 바로 그 오류입니다!

원인 찾아 삼만리

register의 타입을 확인해보기~

  • registerusrForminput 요소를 등록하는 역할을 합니다.
  • UseFormRegister라는 함수 타입으로 정의되어 있습니다.

이어서 UseFormRegister 타입 확인

  • 여기에서 TFieldName이 나타납니다.
  • TFieldNameregisterinputname을 나타내는 제너릭 타입으로, FieldPath<TFieldValues> 타입을 extends한 타입입니다.
  • <TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>
    ⇒ 사용자가 TFieldName을 지정하지 않는다면, FieldPath<TFieldValues> 타입이 기본값으로 사용된다는 것을 의미합니다.
  • 이렇게 정의된 TFieldNameregister의 첫 번째 매개변수인 input 요소의 name을 나타내는 타입으로 사용됩니다.

FieldPath<TFieldValues> 다음과 같이 타입이 정의되어 있습니다.

  • Path<TFieldValues>TFieldValues 객체의 모든 키를 문자열 리터럴로 연결한 타입입니다.
    TFieldValues 객체의 키를 "key1.key2.key3...."와 같은 형태로 표현합니다.
  • RegisterSchemaFieldPath에 적용한다면 FieldPath<RegisterSchema>"email" | "username" | "password" | "passwordCheck" | "imgUrl" | "termsAgreement" | "termsAgreement.service" | "termsAgreement.privacy" | "termsAgreement.marketing" 형식이 됩니다.
    ⇒ 어디서 많이 본 형식이네요.

TFieldValuesFieldValuesextends한 타입인데요. FieldValues를 확인해보면 다음과 같습니다.

  • 키는 string 타입이고, 값은 any 타입이므로 값으로는 어떤 타입이든 가능합니다.

결국 register의 첫 번째 매개변수인 input 요소의 name의 타입은 string 이지만 Path<TFieldValues>에 의해 TFieldValues 객체의 키를 문자열 리터럴로 연결하면서 "email" | "username" | "password" | "passwordCheck" | "imgUrl" | "termsAgreement" | "termsAgreement.service" | "termsAgreement.privacy" | "termsAgreement.marketing" 타입으로 타입이 좁혀진 것 같습니다.

제가 적용한 const fieldName = `termsAgreement.${id}`; 의 경우 "termsAgreement.service" | "termsAgreement.privacy" | "termsAgreement.marketing" 타입이 아닌 string 타입으로 추론되어 오류가 발생한 것입니다!! 끄아아아!!!!!!!!

해결 방법

그래서 해결 방법 중 하나는 RegisterSchema 에 인덱스 시그니처를 추가하는 것입니다.

export interface RegisterSchema {
  email: string;
  username: string;
  password: string;
  passwordCheck: string;
  imgUrl: string;
  termsAgreement: {
    service: boolean;
    privacy: boolean;
    marketing: boolean;
  };
  [key: string]: any; // 인덱스 시그니처 추가
}

이렇게 하면 오류가 사라집니다. 그리고 오타나 잘못된 string 키를 사용해도 오류를 발생시키지 않습니다.

그래서 저는 다음과 같이 FieldPath<RegisterSchema> 의 형식에서 허용하는 유니온 타입을 정의하여 오류가 발생하지 않도록 했습니다.

// `FieldPath<RegisterSchema>`의 형식에서 허용하는 유니온 타입을 정의
type TermsAgreementField = 'termsAgreement.service' | 'termsAgreement.privacy' | 'termsAgreement.marketing';
// TermsAgreementField 타입 적용
<CheckboxList>
  {TERMS_AND_CONDITIONS.map((term) => {
    const { id, title, required } = term;
    // fieldName의 타입을 TermsAgreementField 타입으로 단언
    const fieldName = `termsAgreement.${id}` as TermsAgreementField; 
    return (
      <li key={`terms-and-conditions-${id}`}>
        <CheckboxInput
          id={id}
          type="checkbox"
          checked={agreedToTerms[id]}
          {...register(fieldName, { // 타입 단언으로 오류 발생하지 않음!
            required: !!required,
            onChange: handleOnToggleCheckbox,
          })}
        />
      </li>
    );
  })}
</CheckboxList>

마무리

타입 오류가 발생했을 때는 정의된 타입을 직접 확인하고 해당 타입이 어떤 타입인지 이해하는 것이 확인한 후 적절한 방법을 찾아내는 것이 중요한 것 같습니다.
d.ts 파일의 정의된 타입을 봐도 아직 모르는 부분이 많아 중간에 어려운 부분은 Chat GPT에게 질문하여 답변을 통해 이해했습니다.
그렇다고 모든 것을 이해한 것은 아니지만, 이전보다는 많이 이해했으니 만족입니다!
그리고 틀린 부분이 있다면 알려주세요. 헿

0개의 댓글