My Boundary As Much As I Experienced

typescript) 제네릭을 이용하여 공통 컴포넌트의 타입을 관리해보자 본문

FrontEnd/TypeScript

typescript) 제네릭을 이용하여 공통 컴포넌트의 타입을 관리해보자

Bumang 2024. 6. 18. 19:06

지금 만드는 토큰 환전 페이지에서 공통 컴포넌트인 Input을 사용해야되는 상황이 있었다.

input은 사용자 입력이 올바르지 않을 경우 에러 메시지를 표출한다.

이때 인풋의 타입이 NUMBER인지 STRING인지에 따라 에러메시지가 달랐다.

 

처음에는 이를 아래처럼 유니언 타입으로 설계하였다.

message: NUMBER_VALUE | STRING_VALUE;
setMessage: (value: NUMBER_VALUE | STRING_VALUE) => void;

 

 

유니온 타입의 한계

 

그런데 위처럼 유니언 타입으로 설계하면 타입스크립트가 항상 다른 타입의 가능성을 경고하게된다.

예를 들어 NUMBER_VALUE를 쓸 때 타스가 STRING_VALUE의 가능성을 계~속 경고한다...

 

if (message typeof NUMVER_VALUE) ... 같은 식으로

어찌저찌 message의 타입을 단언하면 해결될 줄 알았으나

setMessage에서도 STRING_VALUE의 가능성이 있다고 계속 경고한다...

 

message가 NUMBER_VALUE이면

setMessage의 파라미터도 NUMBER_VALUE일거라 받아들이는건

인간 프로그래머인 나 뿐이다. 타스는 절대 용납 안 한다.

 

setMessage(value as NUMBER_VALUE)라고 단언하려해도

'아닌데? STRING_VALUE가 될 수 있다고 인터페이스에 명시되어 있는데?'라며

에러를 멈추지 않는다.

 

이대로 계속 유니온 타입을 쓰면 message값이나 setMessage함수를 쓸 때

거추장스러운 타입 가드와 타입 단언을 매우매우 많이 쓰게 될 것이고,

해결 못하는 경고도 발생할 것이다.

 

해결법

그래서 제네릭을 한 번 써보기로 했다.

국룰인 대문자 T를 쓰고, 'extends NUMBER_VALUE | STRING_VALUE'로 T의 범위를 한정시켰다.

 

그리고 T가 둘 중에 뭐가 될지 모르겠지만,

T의 타입이 한 번 정해지면 message와 setMessage에서 동일한 타입을 쓰도록 설계하였다.

interface InputProps<T extends NUMBER_VALUE | STRING_VALUE> {
  ...
  message: T;
  setMessage: (str: T) => void;
}

 

그리고 setMessage를 쓸 때 as T로 타입 단언을 해주면 된다.

setMessage(ERROR["NUMBER"][0] as T);

 

 

전체 코드

interface InputProps<T extends NUMBER_VALUE | STRING_VALUE> {
  ...
  message: T;
  setMessage: (str: T) => void;
}

const Input = <T extends NUMBER_VALUE | STRING_VALUE>({ type, onChange, value, ceil, mt, mb, placeHolder, messageRef, inputRef, message, setMessage, ...props }: InputProps<T>) => {
  const HandleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setMessage(ERROR["NUMBER"][0] as T);
    if (type === "NUMBER") {
      ...
    }

    if (type === "STRING") {
      ...
    }
  };

  const handleBlurMessage = () => {
    if (message === ERROR["NUMBER"][3]) return;
    setMessage("" as T);
  };

  return (
    ...
  );
};

export default Input;