Description
TypeScript
Generic Types & Conditional Types
useFetch hook을 정의하는 데 사용했습니다.
useFetch Full Definition
//#region Type Definition
type DataType<T, S> = S extends undefined ? T : S;
type ArgType<T, S> = {
urlSegment: string;
modifier?: (arg: T) => DataType<T, S>;
callbacks?: ((arg: DataType<T, S>) => void | Promise<void>)[]; // What if S is provided but callbacks need T?
additionalDeps?: DependencyList;
additionalCondition?: boolean;
};
type CustomReturnType<T, S> = [
DataType<T, S> | null,
Dispatch<SetStateAction<DataType<T, S> | null>>
];
//#endregion
/**
* useFetch<T, S = undefined>에 대해, S가 어떻게 쓰이는지?
*
* useFetch에 의해 fetch되는 데이터를 약간 수정해서 이용해야 하는 경우가 존재한다.
* 이때 그 수정된 데이터가 기존의 타입 T와 다른 경우, S를 이용해 그 수정된 데이터의 타입을 명시한다.
* e.g) the useFetch call in the client/src/Pages/Statistics/Statistics.tsx
*
* callbacks: fetch된 데이터 혹은 추후 수정된 데이터를 argument로 하여 call되는 callback함수들의 array.
*
* return type은 S가 정의된 경우 S 그렇지 않으면 T이다.
* callbacks은 return type관여하지 않는다.
*
*/
export function useFetch<T, S = undefined>({
urlSegment,
modifier,
callbacks,
additionalDeps,
additionalCondition,
}: ArgType<T, S>): CustomReturnType<T, S> {
const [data, setData] = useState<DataType<T, S> | null>(null);
const { user } = useAuthContext()!;
let moreDeps: DependencyList = additionalDeps ?? [];
useEffect(() => {
if (isUserSignedIn() && isAdditionalConditionSatisfiedWhenProvided()) {
getData();
}
/**
* Purpose: to get data from either remote server or cache storage.
*/
async function getData() {
let resData = await caches.match(BASE_URL + urlSegment);
if (resData) {
let data: DataType<T, S> =
modifier !== undefined
? modifier((await resData.json()) as T)
: await resData.json();
console.log("data from cache", data);
if (callbacks !== undefined) {
for (const fn of callbacks) {
await fn(data);
}
}
setData(data);
} else {
let res = await fetchDataFromServer();
if (res !== undefined) {
let resFetched = new Response(JSON.stringify(res.data));
let cache = DynamicCache || (await openCache(CacheName));
await cache.put(BASE_URL + urlSegment, resFetched);
}
}
}
// This is going to be used in the getData() function.
async function fetchDataFromServer() {
try {
const response = await axiosInstance.get(urlSegment);
let data =
modifier !== undefined ? modifier(response.data as T) : response.data;
if (callbacks !== undefined) {
for (const fn of callbacks) {
await fn(data);
}
}
setData(data);
return response;
} catch (error) {
console.warn(error);
}
}
function isUserSignedIn() {
return user !== null;
}
function isAdditionalConditionSatisfiedWhenProvided() {
return additionalCondition ?? true;
}
}, [user, ...moreDeps]);
return [data, setData];
}
사용한 방식
useFetch는 다음과 같이 T와 S, 두 가지 Generic 타입을 받는 함수입니다.
function useFetch<T, S = undefined>({
urlSegment,
modifier,
callbacks,
additionalDeps,
additionalCondition,
}: ArgType<T, S>): CustomReturnType<T, S>
- 이 함수에서
ArgType<T, S>와CustomReturnType<T, S>는 T와 S에 영향을 받는데, - 두 타입 모두 결과적으로는
DataType<T, S>에 의해서만 영향을 받게 됩니다. DataType<T, S>은 useFetch가 정의된S와 함께 호출되면S를, 그렇지 않으면T값을 갖는 Conditional Type입니다.
타입 정의
type ArgType<T, S> = {
urlSegment: string;
modifier?: (arg: T) => DataType<T, S>;
callbacks?: ((arg: DataType<T, S>) => void | Promise<void>)[]; // What if S is provided but callbacks need T?
additionalDeps?: DependencyList;
additionalCondition?: boolean;
};
type CustomReturnType<T, S> = [
DataType<T, S> | null,
Dispatch<SetStateAction<DataType<T, S> | null>>
];
type DataType<T, S> = S extends undefined ? T : S;
사용한 이유
서버에서 직접 받아오는 데이터의 type은 T인데, 때에 따라서 이 값을 직접 return하지 않고 중간에
modify해야하는 경우가 있습니다. modify한다고 해서 항상 그 결과 값의 타입이 변하는 것은 아니지만,
제가 사용하는 use case에서는 그런 경우가 존재했고 그래서 이것이 일반적인 경우라고 가정하여, S라는
두 번째 타입을 정의했습니다. modify했을 때 결과 값의 달라지는 타입이 S이고 선택적으로 명시해주면 됩니다.
참고로 callbacks는 data를 사용하는 처지만을 갖고, 수정하는 권한은 없다고 정의했습니다.
예시
/statistics에서 render되는 Statistics component는 pomodoro data를 서버로부터 가져옵니다.
const [statData, setStatData] = useFetch<
PomodoroSessionDocument[],
DayStat[]
>({
urlSegment: RESOURCE.POMODOROS,
modifier: calculateDailyPomodoroDuration,
callbacks: [calculateWeeklyTrend, calculateOverview, calculateThisWeekData],
});
타입 정의
// T
export type PomodoroSessionDocument = {
userEmail: string;
duration: number;
startTime: number;
date: string;
category?: CategoryForStat;
};
// S
export type DayStat = TimeRelated & DurationRelated;
type TimeRelated = {
date: string;
timestamp: number;
dayOfWeek: string;
weekNumber: number;
};
type DurationRelated = {
total: number;
subtotalByCategory: CategorySubtotal;
withoutCategory: number;
};