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를 활용한 이벤트 유실 예방하기 (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 |