Discriminated Union 설명
discriminated union은 type narrowing을 하기 위한 하나의 방법입니다. primitive type보다 더 복잡한 타입들을 narrow하기 위해 자주 사용됩니다. 말보단 코딩, 바로 코드로 살펴보죠.
토이 프로젝트로 sns 앱을 만들었고 피드에 올라갈 수 있는 것으로 포스팅과 광고가 있다고 해보겠습니다. Feed
라는 타입을 만들고 type
필드를 통해 이를 구별하도록 했습니다. 만약 포스팅이라면 달린 댓글들이 있을 것이고 광고라면 광고주에 대한 정보가 있겠죠?따라서 이를 optional로 처리하였습니다.
interface Feed {
type: 'post' | 'advertisement';
advertiser?: string;
comments?: string[];
}
피드가 광고일 경우 광고주 데이터를 가져와보죠.
const getAdvertiser = (feed: Feed) => {
if (feed.type === 'advertisement') {
console.log(feed.advertiser) // string | undefined
}
}
이상한 점이 있습니다. 피드가 분명 광고인데도 feed.advertiser
가 undefined
일 수 있다는 점입니다. optional로 정의했기 때문이죠. 바로 여기서 discriminated union을 사용하면 좀 더 명시적인 타입 설계를 할 수 있습니다.
interface Advertisement {
type: 'advertisement'
advertiser: string;
}
interface Post {
type: 'post';
commnets: string[]
}
type Feed = Advertisement | Post
const getAdvertiser = (feed: Feed) => {
if (feed.type === 'advertisement') {
console.log(feed.advertiser)
}
}
Discriminated Union - React Query
요즘 비동기 처리를 위해 React Query를 많이 사용하죠? React Query에서도 discriminated union을 사용한 예시를 찾아 볼 수 있습니다.
export interface QueryObserverLoadingResult<TData = unknown, TError = unknown>
extends QueryObserverBaseResult<TData, TError> {
data: undefined
error: null
isError: false
isLoading: true
isLoadingError: false
isRefetchError: false
isSuccess: false
status: 'loading'
}
export interface QueryObserverLoadingErrorResult<
TData = unknown,
TError = unknown,
> extends QueryObserverBaseResult<TData, TError> {
data: undefined
error: TError
isError: true
isLoading: false
isLoadingError: true
isRefetchError: false
isSuccess: false
status: 'error'
}
export interface QueryObserverRefetchErrorResult<
TData = unknown,
TError = unknown,
> extends QueryObserverBaseResult<TData, TError> {
data: TData
error: TError
isError: true
isLoading: false
isLoadingError: false
isRefetchError: true
isSuccess: false
status: 'error'
}
export interface QueryObserverSuccessResult<TData = unknown, TError = unknown>
extends QueryObserverBaseResult<TData, TError> {
data: TData
error: null
isError: false
isLoading: false
isLoadingError: false
isRefetchError: false
isSuccess: true
status: 'success'
}
export type DefinedQueryObserverResult<TData = unknown, TError = unknown> =
| QueryObserverRefetchErrorResult<TData, TError>
| QueryObserverSuccessResult<TData, TError>
export type QueryObserverResult<TData = unknown, TError = unknown> =
| DefinedQueryObserverResult<TData, TError>
| QueryObserverLoadingErrorResult<TData, TError>
| QueryObserverLoadingResult<TData, TError>
위와 같은 타입 설계 덕분에 컴포넌트에서 useQuery
를 사용했을때 data
의 타입을 좁힐 수 있습니다.
const { isLoading, isError, data }= useQuery(//...)
if (isLoading) return <div>loading...</div>
if (isError) return <div>error...</div>
return <div>{data}</div>
마치며
최근 TypeScript를 다시 공부하면서 느꼈던게 타입 설계를 얼마나 잘하냐에 따라서 어플리케이션 코드의 복잡도를 낮출 수 있는 것 같습니다. discriminated union뿐만 아니라 generic, mapped type 그리고 conditional type까지 여러 기능들을 지원하는 이유는 결국 타입도 코드와 마찬가지로 SRP, DRY 등과 같은 원칙들을 준수하는데 있어 좀 더 용이한 방법을 지원하는게 아닌가 싶습니다.
한 걸음 더 높은 수준의 코드를 작성하기 위해 앞으로 이러한 타입 설계 방법들에 대해 포스팅해보도록 하겠습니다.
References
'Frontend' 카테고리의 다른 글
Next.js proxy를 활용해 Analytics 이벤트를 수집해보자 (0) | 2023.02.21 |
---|---|
디자인 패턴(Singleton, Facade)을 사용한 Analytics 코드설계 (0) | 2023.01.15 |
Turborepo 도입하면서 겪었던 이슈들 (0) | 2022.11.14 |
Next.js 13 버전을 알아보자 - Turbopack, Layouts, Server Components... (0) | 2022.10.29 |
Cypress를 GitHub Actions에 연동해보자! (0) | 2022.10.07 |