타입 안전성과 TypeScript의 조건 검사
TypeScript는 정적 타입 시스템을 제공하여 컴파일 타임에 오류를 잡아낼 수 있도록 설계된 언어다. 하지만 일반적인 조건문에서는 논리적으로 불가능한 분기를 검사하더라도, 기본적으로 컴파일 오류를 발생시키지 않는다. 이로 인해 런타임에서는 도달해서는 안 되는 경로에 도달할 수 있으며, 이를 막기 위해 개발자는 명시적으로 "불가능한 조건"을 컴파일 타임에 오류로 잡아낼 수 있는 패턴을 구현해야 한다.
왜 불가능한 조건을 TypeScript에서 막아야 하는가
- 정적 타입 검사의 강력함 활용
- 런타임 오류 방지
- 비정상 흐름을 명시적으로 확인
- 유지보수성과 협업에서의 안전성 확보
이를 위해 우리는 타입 수준에서 절대 일어날 수 없는 조건을 의도적으로 컴파일 오류로 처리하는 전략을 활용해야 한다.
불가능한 조건을 에러로 유도하는 패턴
1. never 타입을 활용한 조건 차단
TypeScript에서 never는 절대 발생하지 않아야 하는 타입을 의미한다. 이 특징을 활용하면, 코드의 특정 분기에서 논리적 불가능함을 명시적으로 컴파일 오류로 전환할 수 있다.
function assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}`);
}
이 함수를 조건 분기 마지막에서 사용하면, 모든 케이스를 다 다루지 않으면 컴파일 에러를 발생시킨다.
type Shape = "circle" | "square";
function getArea(shape: Shape) {
switch (shape) {
case "circle":
return Math.PI * 2;
case "square":
return 4;
default:
return assertNever(shape); // shape가 never가 아니면 컴파일 에러
}
}
2. 조건부 타입으로 발생 불가능 조건 명시
조건부 타입을 사용해 논리적으로 불가능한 경우를 타입 시스템에서 표현할 수 있다.
type Impossible<T extends false> = T;
type Case1 = Impossible<true>; // 오류 발생
type Case2 = Impossible<false>; // 정상
이 패턴은 타입이 false일 때만 정상 작동하게 하여, 불가능한 조건이 발생하면 컴파일 오류가 발생한다.
3. 제네릭과 조건부 검사를 통한 에러 유도
함수나 클래스의 인자 타입에 제약을 걸어, 의도한 조건을 벗어날 경우 컴파일 타임에 에러를 발생시킨다.
type ExpectTrue<T extends true> = T;
type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2) ? true : false;
type Test = ExpectTrue<IsEqual<"a", "b">>; // 컴파일 에러 발생
조건 충족 여부를 강제하는 유틸리티 타입 전략
4. 유효한 케이스만 통과시키는 Discriminated Union
Discriminated Union은 특정 프로퍼티 값을 기준으로 타입 분기를 유도할 수 있어, 명시적 케이스 분리가 가능하다.
type Action =
| { type: "increment"; amount: number }
| { type: "decrement"; amount: number };
function reducer(action: Action) {
switch (action.type) {
case "increment":
return action.amount + 1;
case "decrement":
return action.amount - 1;
default:
return assertNever(action); // Action의 다른 타입이 생기면 컴파일 에러
}
}
5. 리터럴 조건 조합을 통한 불가능 조건 선언
리터럴 조합과 never를 활용하면, 타입 수준에서 상호 배타적인 조건을 강제할 수 있다.
type A = { kind: "a", aProp: string };
type B = { kind: "b", bProp: number };
type C = A & B; // C는 never가 됨
const obj: C = {
kind: "a",
aProp: "hello",
bProp: 42, // 오류 발생
};
조건 검증 시 활용 가능한 도구 및 기법
6. 컴파일 타임 assertion 함수 패턴
커스텀 assertion 함수를 만들어 불가능한 상태에 대해 정적 검사를 강제한다.
function assertType<T extends true>() {}
type Check = IsEqual<1, 2>; // false
assertType<Check>(); // 오류 발생
이 기법은 테스트 목적이거나, TS 5.x 이후의 advanced type-checking에 매우 유용하다.
7. satisfies 연산자를 활용한 타입 협의 검사 (TS 4.9 이상)
satisfies는 객체 리터럴이 특정 타입을 만족하는지를 검사하며, 보다 강력한 타입 검증이 가능하다.
const obj = {
name: "abc",
value: 123
} satisfies { name: string; value: number; };
잘못된 속성이 들어오면 타입 오류가 발생한다.
에러 유도 전략의 실제 응용 사례
8. API 응답 모델의 스키마 검사
서버 API 응답 타입을 정확히 정의하고, 예외적인 경우를 never로 처리하여 의도치 않은 응답에 오류 발생을 유도한다.
type APIResponse =
| { status: "ok"; data: string }
| { status: "error"; message: string };
function handleResponse(res: APIResponse) {
switch (res.status) {
case "ok":
return res.data;
case "error":
return res.message;
default:
return assertNever(res); // status 필드에 새로운 값이 생기면 에러
}
}
9. 구성 객체의 값 제약 조건 강제
type Config = {
mode: "light" | "dark";
theme: never; // 존재해서는 안 되는 속성
};
const badConfig: Config = {
mode: "light",
theme: "classic", // 컴파일 에러 발생
};
이 방식은 특정 조합을 완전히 배제하는 데 유용하다.
맺음말
- never 타입을 활용하여 의도적으로 컴파일 오류 유도
- 조건부 타입 (T extends false, IsEqual<A,B>)으로 논리 체크
- Discriminated Union 및 Exhaustive switch 패턴으로 누락 방지
- 커스텀 assertion 함수와 assertType<T>()을 통한 정적 검사
- satisfies 연산자를 통해 구조적 타입 체크 강화
- 잘못된 조합 자체를 never로 선언하여 허용 불가 선언
'delphi' 카테고리의 다른 글
Jetpack Compose HorizontalPager에서 NavController로 화면 이동하는 방법 (0) | 2025.06.20 |
---|---|
AWS S3에서 대용량 CSV 파일을 효율적으로 스트리밍하고 라인별로 처리하는 방법 (0) | 2025.06.20 |
Spring Boot ApplicationContextException Unable to Start Web Server 해결 가이드 (0) | 2025.06.19 |
PHP GoCardless Pro Uncaught Error Class GoCardlessPro\Client Not Found 해결 가이드 (0) | 2025.06.19 |
TListView에서 TListItem의 위치를 변경하는 방법 (0) | 2024.08.16 |