TypeScript Type Guards for Discriminated Unions (Scalable Code를 위한 Best Practices)
Source: Dev.to
번역할 텍스트가 제공되지 않았습니다. 번역이 필요한 본문을 알려주시면 도와드리겠습니다.
이 가이드에서 다룰 내용
- 차별화된 유니언이란 무엇인가
- 타입 가드가 중요한 이유
- 실제 애플리케이션에서 사용하기 위한 모범 사례
- 개발자들이 흔히 저지르는 실수
문제: 여러 데이터 형태 처리
많은 애플리케이션에서 변수는 다양한 종류의 객체를 나타낼 수 있습니다 (예: API 응답 상태).
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
이는 유니온 타입입니다.
하지만 TypeScript는 어떤 속성이 존재하는지 어떻게 알까요?
function handleResponse(res: ApiResponse) {
console.log(res.data);
}
TypeScript 오류
Property 'data' does not exist on type 'ApiResponse'.
왜냐하면 모든 유니온 멤버가 data를 가지고 있는 것은 아니기 때문입니다.
Solution: Discriminated Unions
A discriminated union uses a common property (the discriminator) to identify the type. In our example the discriminator is status.
Now TypeScript can narrow types safely:
function handleResponse(res: ApiResponse) {
if (res.status === "success") {
console.log(res.data);
}
}
TypeScript automatically knows that inside the if block res is:
{ status: "success"; data: string[] }
This is called type narrowing.
타입 가드란 무엇인가?
타입 가드는 TypeScript가 정확한 타입을 판단하도록 돕는 로직입니다.
if (res.status === "error") { /* … */ }
이 조건은 타입 가드 역할을 합니다.
또한 사용자 정의 타입 가드를 만들 수도 있습니다.
Custom Type Guards 만들기
Custom guards는 가독성과 재사용성을 향상시킵니다.
function isSuccess(
res: ApiResponse
): res is { status: "success"; data: string[] } {
return res.status === "success";
}
사용법
if (isSuccess(res)) {
console.log(res.data); // `res`는 자동으로 좁혀집니다
}
이는 대규모 애플리케이션에서 매우 유용합니다.
실제 예시: 결제 시스템
응답이 서로 다른 결제 시스템을 고려해 보세요.
type PaymentResult =
| { type: "success"; transactionId: string }
| { type: "failed"; error: string }
| { type: "pending"; estimatedTime: number };
구분된 유니온을 사용하면:
function handlePayment(result: PaymentResult) {
switch (result.type) {
case "success":
console.log(result.transactionId);
break;
case "failed":
console.log(result.error);
break;
case "pending":
console.log(result.estimatedTime);
break;
}
}
type 필드는 구분자 역할을 합니다.
Best Practice 1: 항상 단일 판별자 속성을 사용하세요
일반적인 판별자 이름:
type
kind
status
variant
예시
type Shape =
| { type: "circle"; radius: number }
| { type: "square"; size: number };
{ kind: "circle", shape: "circle" }와 같은 다중 판별자를 피하세요.
하나의 명확한 속성을 사용하세요.
모범 사례 2: 여러 if 문 대신 switch 사용
switch 문은 가독성 및 유지보수성을 향상시킵니다.
잘못된 예
if (shape.type === "circle") {}
if (shape.type === "square") {}
좋은 예
switch (shape.type) {
case "circle":
// …
break;
case "square":
// …
break;
}
이렇게 하면 완전한 타입 검사도 가능해집니다.
모범 사례 3: Exhaustive Checks 사용
강력한 TypeScript 기법입니다.
function assertNever(x: never): never {
throw new Error("Unexpected type");
}
예시
switch (shape.type) {
case "circle":
// …
break;
case "square":
// …
break;
default:
assertNever(shape);
}
새로운 타입이 추가되면(예: { type: "triangle" }), TypeScript는 즉시 오류를 발생시켜 조용한 버그를 방지합니다.
모범 사례 4: 유니온 타입에서 선택적 필드 피하기
잘못된 설계
type ApiResponse = {
status: "success" | "error";
data?: string[];
error?: string;
};
이 경우 불명확한 상태가 생성됩니다.
더 나은 설계
type ApiResponse =
| { status: "success"; data: string[] }
| { status: "error"; error: string };
이제 타입 시스템이 유효한 상태만 강제합니다.
베스트 프랙티스 5: UI 상태에 차별화된 유니언 사용
type LoadingState =
| { status: "idle" }
| { status: "loading" }
| { status: "error"; message: string };
Best Practice 5 – UI 상태에 차별화된 유니언 사용
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; message: string };
UI에서 사용하기
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return state.data;
case "error":
return state.message;
}
이는 잘못된 UI 상태를 방지합니다.
모범 사례 6 – 재사용 가능한 타입 가드 만들기
논리를 곳곳에 반복해서 작성하는 대신, 가드를 한 번 정의하고 재사용하세요.
예시
function isError(res: ApiResponse): res is { status: "error"; message: string } {
return res.status === "error";
}
사용법
if (isError(response)) {
console.log(response.message);
}
재사용 가능한 가드는 클린 아키텍처를 개선합니다.
모범 사례 7 – 유니온 타입을 작고 집중적으로 유지하기
극도로 큰 유니온(예: “50가지 다른 변형”)을 피하십시오.
이를 논리적 그룹으로 나누세요.
예시
// Separate concerns
type UserState = { /* ... */ };
type OrderState = { /* ... */ };
type PaymentState = { /* ... */ };
이는 코드를 유지 보수 가능하게 합니다.
개발자들이 흔히 하는 실수
-
any사용하기function handle(res: any) { /* ... */ }왜 안 좋은가: TypeScript의 안전성을 없애버립니다.
-
완전한 검사 누락
개발자는 종종 새로운 유니온 케이스를 처리하는 것을 잊습니다.
항상assertNever헬퍼를 사용하세요:function assertNever(x: never): never { throw new Error(`Unexpected object: ${x}`); } -
관련 없는 유니온 혼합
type Result = | { type: "user" } | { type: "product" } | { type: "error" };왜 안 좋은가: 서로 관련 없는 도메인 관점을 하나로 합칩니다.
해결책: 각각을 별개의 타입으로 분리합니다.
차별화된 유니언이 강력한 이유
- 불가능한 상태 방지
- 스스로 문서화되는 코드 작성
- 컴파일 시 오류 감지
- 대규모 코드베이스의 유지보수성 향상
차별화된 유니언은 다음 분야에서 많이 사용됩니다:
- Angular 상태 관리
- Redux / NgRx
- API 응답 모델링
- 도메인 주도 설계
최종 생각
Discriminated unions + type guards는 TypeScript에서 가장 강력한 패턴 중 하나입니다.
이들은 real‑world state transitions safely를 모델링하면서 코드의 가독성과 확장성을 유지하도록 해줍니다.
large TypeScript applications를 구축하고 있다면, 이 패턴을 마스터하면 code quality and reliability가 크게 향상됩니다.