
회사에서 특정 파일의 업로드와, 업로드 성공 여부에 따라 알림 전송 가능 여부를 결정해야 하는 기능을 구현해야 했다.
우선, 업무 규칙만 모아놓은 커스텀 훅을 구현했다.
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는 그 사실을 알지 못해서 타입 에러가 발생!
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] 로 추론되어, 인덱스를 통해 원소를 뽑아오면 타입 에러가 발생하지 않는다.