
발단
회사에서 특정 파일의 업로드와, 업로드 성공 여부에 따라 알림 전송 가능 여부를 결정해야 하는 기능을 구현해야 했다.
우선, 업무 규칙만 모아놓은 커스텀 훅을 구현했다.
커스텀 훅
function useFooBar() { // 초기값은 null로 설정! const [fooResult, setFooResult] = useState<AxiosResponse | null>(null); const [barResult, setBarResult] = useState<AxiosResponse | null>(null); // 각 axios 결과가 200 이어야 한다. const isUploadingFooSuccess = fooResult?.status === 200; const isUploadingBarSuccess = barResult?.status === 200; // BE 에 notify 요청을 보낼 수 있는 조건 const isCanNotify = isUploadingFooSuccess && isUploadingBarSuccess; return { // ... isCanNotify }; }
useFooBar
커스텀 훅에서는 nullable 상태인 fooResult
와 barResult
를 가지고 있으며, 이 두 값 모두 status
속성이 200
이라면 notify 를 할 수 있다.컴포턴트
function UploadFooBar() { const { isCanNotify, } = useFooBar(); const uploadFooBarForPreProcess = () => { // isCanNotify 를 이용해 early return 적용! if (!isCanNotify) { showErrorToast(new Error('전처리에 필요한 파일이 정상적으로 업로드되지 않았습니다. 다시 업로드 해주세요.')); return; } // 이 시점부터 fooResult 와 barResult 는 non-null 이다. Promise.all([ notifyFoo(fooResult.config.data.name), // TS2531: Object is possibly 'null'. notifyBar(barResult.config.data.name), // TS2531: Object is possibly 'null'. ]) // ... }; return ( <form onSubmit={uploadFooBarForPreProcess}> /* ... */ </form> ); }
isCanNotify
를 통해 개발자는 fooResult
와 barResult
가 null
이 아님을 알고 있지만, 타입가드를 해주지 않았기 때문에, TS는 그 사실을 알지 못해서 타입 에러가 발생!왜 mutiple type guard 는 미지원일까...
TS에게 이러한 값들이 non-nullable 타입임을 알려주기 위해서는 다양한 type narrowing 기술을 사용해야 한다.
그 중에서 나는 type guard를 애용하는 편이다. (커스터마이징이 편해서..)
그런데 문제는 지금 타입 가드를 해야 할 변수가 2개 이상이라는 점이다. 현재 TS 에서는 multiple argument 에 대해 type guard 를 지원하지 않기 때문에, 편법을 써야 한다.
참고 링크 무려 2018년부터 요구사항이 있었지만 대응이 없는.. 🫠
인자를 단일화해서 해결!
결국 구글링해서, 배열을 이용해 단일 인자화 한 뒤, 타입 가드하는 방법을 찾게 되었다.
function useFooBar() { // 두 상태 모두 사용할 수 있는 범용 타입 가드 함수 구현 function isUploadingFooBarSuccess<T extends AxiosResponse>( fooBarResult: T | null, ): fooBarResult is Exclude<T, null> { return fooBarResult?.status === 200; } // T | null 타입의 튜플을 받는 타입 가드 함수로 변경. // 만약 타입 가드를 통과한다면, 인자가 [T, T] 타입임을 보장한다. function isCanNotify<T extends AxiosResponse>(args: [T | null, T | null]): args is [T, T] { return args.every(isUploadingFooBarSuccess); } // ... }
기존의 단순 boolean 변수 대신에
isUploadingFooBarSuccess
라는 범용 함수를 구현하고, isCanNotify
또한 타입 가드 함수로 바꿨다.여기서,
args
를 [T | null, T | null]
이라는 튜플 타입으로 정의하고, 반환 시그니처를 args is [T, T]
로 설정하여 반환된 튜플의 원소가 모두 T
임을 보장하게 했다.이제 수정된 코드를 실제 로직에 적용해보자.
function UploadFooBar() { const uploadFooBarForPreProcess = () => { // isCanNotify 타입 가드의 인자로 넣기 위한 변수를 선언 및 할당 const uploadingFooBarList: [AxiosResponse | null, AxiosResponse | null] = [ fooResult, barResult, ]; // 타입 가드! if (!isCanNotify(uploadingFooBarList)) { showErrorToast(new Error('전처리에 필요한 파일이 정상적으로 업로드되지 않았습니다. 다시 업로드 해주세요.')); return; } // 타입 에러가 발생하지 않는다! Promise.all([ notifyFoo(uploadingFooBarList[0].config.data.name), notifyBar(uploadingFooBarList[1].config.data.name), ]) // ... }; // ... }
개선된 코드에서는
uploadingFooBarList
라는 튜플 변수를 만든 뒤, isCanNotify
를 통해 타입 가드를 처리한다.그러면 early return 밑의 영역에서는
uploadingFooBarList
의 타입이 [AxiosResponse, AxiosResponse]
로 추론되어, 인덱스를 통해 원소를 뽑아오면 타입 에러가 발생하지 않는다.