원문 보기 : https://tkdodo.eu/blog/react-query-and-type-script
리액트 쿼리의 동적이며 unopinated인 특성 때문에 ts와 사용하기 어려운 부분들이 존재합니다. 좀 더 알아봅시다.
Generics
React Query는 Generics를 많이 사용합니다.
라이브러리가 실제로 데이터를 가져오지 않고 API가 반환하는 데이터의 타입을 알 수 없기 때문에 필요합니다.
공식 문서(official docs)의 TypeScript 섹션은 그리 길지 않으며,
useQuery가 호출할 때 예상하는 Generics를 명시적으로 지정하도록 알려줍니다.
아래 예제의 경우 다음과 같이 타입을 추론합니다
- 리턴 데이터의 타입은 Group[] | undefined
- 오류의 타입은 Error | undefined
function useGroups() {
return useQuery<Group[], Error>('groups', fetchGroups)
}
하지만 useQuery훅에는 점점 많은 제네릭이 추가되어 현재 4개가 있습니다.
The four Generics
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
- TQueryFnData: queryFn에서 반환된 타입입니다. 이전 예제에서 Group[]입니다.
- TError: queryFn에서 예상되는 오류 타입입니다. 이전 예제에서 Error입니다.
- TData: 데이터 속성이 궁극적으로 갖게 될 입니다. 데이터 속성이 queryFn이 반환하는 것과 다를 수 있으므로 select 옵션을 사용하는 경우에만 관련이 있습니다. 그렇지 않으면 queryFn이 반환하는 것이 무엇이든 기본값이 됩니다.
- TQueryKey: queryFn에 전달된 queryKey를 사용하는 경우에만 관련된 queryKey 타입입니다.
보시다시피 모든 Generics에는 기본값이 있습니다.
즉, 직접 값을 제공하지 않으면 TypeScript가 해당 타입으로 대체합니다.
이것은 JavaScript의 기본 매개변수와 거의 동일하게 동작합니다.
function multiply(a, b = 2) {
return a * b
}
Type Inference
const num = Math.random() + 5 // ✅ `number`
// 🚀 both greeting and the result of greet will be string
function greet(greeting = 'ciao') {
return `${greeting}, ${getName()}`
}
Generics의 경우 일반적으로 사용 사례에서 유추할 수 있습니다. 이는 매우 훌륭합니다.
수동으로 제공할 수도 있지만 대부분의 경우에는 필요하지 않습니다.
function identity<T>(value: T): T {
return value
}
// 🚨 no need to provide the generic
let result = identity<number>(23)
// ⚠️ or to annotate the result
let result: number = identity(23)
// 😎 infers correctly to `string`
let result = identity('react-query')
Partial Type Argument Inference (부분 타입 인수 추론)
function useGroupCount() {
return useQuery<Group[], Error>('groups', fetchGroups, {
select: (groups) => groups.length,
// 🚨 Type '(groups: Group[]) => number' is not assignable to type '(data: Group[]) => Group[]'.
// Type 'number' is not assignable to type 'Group[]'.ts(2322)
})
}
세번째 위치 Generic을 제공하지 않았기 때문에 기본값인 Group[]이 지정되지만,
select 함수에서 number를 반환하여 타입 오류가 발생합니다.
한 가지 조치 방안은 3rd Generic을 추가하는 것입니다.
function useGroupCount() {
// ✅ fixed it
return useQuery<Group[], Error, number>('groups', fetchGroups, {
select: (groups) => groups.length,
})
}
Infer all the things (모든 것을 추론하자)
function useGroups() {
// 🚨 data will be `any` here
return useQuery('groups', () =>
axios.get('groups').then((response) => response.data)
)
}
API 레이어를 쿼리와 분리된 상태로 유지하고 싶다면 암시적인 any를 피하기 위해 타입 정의를 추가해야 합니다.
그래야 React Query가 나머지를 추론할 수 있습니다.
function fetchGroups(): Promise<Group[]> {
return axios.get('groups').then((response) => response.data)
}
// ✅ data will be `Group[] | undefined` here
function useGroups() {
return useQuery('groups', fetchGroups)
}
// ✅ data will be `number | undefined` here
function useGroupCount() {
return useQuery('groups', fetchGroups, {
select: (groups) => groups.length,
})
}
- 더 이상 수동으로 Generics를 지정하지 않아도 됩니다.
- 세 번째(select) 및 네 번째(QueryKey) 제네릭이 필요한 경우에도 잘 작동합니다.
- 더 많은 제네릭이 추가되어도 잘 작동할 것입니다.
- 코드가 덜 혼란스럽고 js에 가깝습니다.
What about error?
throw 5
throw undefined
throw Symbol('foo')
const groups = useGroups()
if (groups.error) {
// 🚨 this doesn't work because: Object is of type 'unknown'.ts(2571)
return <div>An error occurred: {groups.error.message}</div>
}
// ✅ the instanceOf check narrows to type `Error`
if (groups.error instanceof Error) {
return <div>An error occurred: {groups.error.message}</div>
}
타입 좁히기
const { data, isSuccess } = useGroups()
if (isSuccess) {
// 🚨 data will still be `Group[] | undefined` here
}
const groupsQuery = useGroups()
if (groupsQuery.isSuccess) {
// ✅ groupsQuery.data will now be `Group[]`
}
TypeScript 4.6에는 destructured discriminated unions에 대한 제어 흐름 분석이 추가되어 동일하게 타입이 추론됩니다만,control flow analysis for destructured discriminated unions
두번째 사용 방법을 고려해보시기 바랍니다.
Type safety with the enabled option
function fetchGroup(id: number): Promise<Group> {
return axios.get(`group/${id}`).then((response) => response.data)
}
function useGroup(id: number | undefined) {
return useQuery(['group', id], () => fetchGroup(id), {
enabled: Boolean(id),
})
// 🚨 Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
// Type 'undefined' is not assignable to type 'number'.ts(2345)
}
function fetchGroup(id: number | undefined): Promise<Group> {
// ✅ check id at runtime because it can be `undefined`
return typeof id === 'undefined'
? Promise.reject(new Error('Invalid id'))
: axios.get(`group/${id}`).then((response) => response.data)
}
function useGroup(id: number | undefined) {
return useQuery(['group', id], () => fetchGroup(id), {
enabled: Boolean(id),
})
}
Optimistic Updates
useInfiniteQuery
type GroupResponse = { next?: number, groups: Group[] }
const queryInfo = useInfiniteQuery(
'groups',
// ⚠️ explicitly type pageParam to override `any`
({ pageParam = 0 }: { pageParam: GroupResponse['next']) => fetchGroups(groups, pageParam),
{
getNextPageParam: (lastGroup) => lastGroup.next,
}
)
fetchGroups가 GroupResponse를 반환하면 lastGroup은 해당 타입을 멋지게 추론하고,
동일한 타입을 사용하여 pageParam에 주석을 추가할 수 있습니다.
Typing the default query function
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: async ({ queryKey: [url] }) => {
const { data } = await axios.get(`${baseUrl}/${url}`)
return data
},
},
},
})
잘 동작하지만 전체 queryKey가 unknown[]이기 때문에 url은 unknown으로 유추됩니다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: async ({ queryKey: [url] }) => {
// ✅ narrow the type of url to string so that we can work with it
if (typeof url === 'string') {
const { data } = await axios.get(`${baseUrl}/${url.toLowerCase()}`)
return data
}
throw new Error('Invalid QueryKey')
},
},
},
})
unknown이 any과 비교하여 왜 훌륭한지를 잘 보여준다고 생각합니다. (타입 검사 가능)
'FrontEnd' 카테고리의 다른 글
리액트 쿼리 : 쿼리키 (0) | 2022.06.19 |
---|---|
리액트 쿼리 : 웹소켓 (0) | 2022.06.18 |
리액트 쿼리 : 테스트 / 테스팅 (0) | 2022.06.18 |
리액트 쿼리 : 쿼리 상태 체크 (0) | 2022.06.18 |
리액트 쿼리 : 렌더링 최적화 (0) | 2022.06.17 |