TypeScript로 코드를 작성하다 보면 외부에서 들어온 값을 내부 타입으로 안전하게 바꿔야 하는 순간이 자주 생깁니다. API 응답, localStorage, URL query, form 입력값처럼 애플리케이션 바깥에서 들어오는 데이터는 처음부터 믿기 어렵습니다.
이럴 때 자주 쓰는 방식이 타입 가드입니다. 그런데 TypeScript에는 타입 가드와 비슷해 보이지만 사용 방식이 다른 assertion function도 있습니다. 대표적인 형태가 바로 asserts value is User입니다.
이번 글에서는 value is User와 asserts value is User의 차이를 사용부 기준으로 정리해보겠습니다.
value is User는 타입 가드다
먼저 value is User를 반환하는 함수는 사용자 정의 타입 가드입니다. 이 함수는 boolean을 반환합니다.
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "id" in value;
}
사용부에서는 반드시 if 같은 조건문으로 분기해야 TypeScript가 해당 블록 안에서 값을 User로 좁혀줍니다.
if (isUser(value)) {
value.id;
}
즉, 타입 가드는 "이 함수가 true를 반환하면 이 값은 User다"라는 의미입니다. 그래서 filter, 조건식, if 문처럼 boolean 결과가 필요한 곳에 잘 어울립니다.
asserts value is User는 단언 함수다
반면 asserts value is User를 사용하는 함수는 assertion function, 즉 단언 함수입니다.
function assertUser(value: unknown): asserts value is User {
if (typeof value !== "object" || value === null || !("id" in value)) {
throw new Error("Invalid user");
}
}
이 함수는 boolean을 반환하지 않습니다. 대신 "이 함수가 예외 없이 정상 종료됐다면, 이후 코드에서 value를 User로 봐도 된다"는 계약을 TypeScript에게 알려줍니다.
사용부는 훨씬 단순해집니다.
assertUser(value);
value.id;
if 블록으로 감싸지 않아도 assertUser(value) 호출 이후부터 value는 User로 좁혀집니다.
핵심 차이는 사용부의 흐름이다
두 방식의 가장 큰 차이는 타입을 좁히는 위치입니다.
value is User는 boolean을 반환하므로 호출한 쪽에서 조건 분기를 만들어야 합니다.
if (isUser(responseData)) {
console.log(responseData.id);
}
반대로 asserts value is User는 함수 내부에서 실패 흐름을 끊습니다. 검증에 실패하면 throw하고, 통과하면 이후 코드는 검증된 값이라는 전제로 이어집니다.
assertUser(responseData);
console.log(responseData.id);
그래서 assertion function은 "이 지점 이후부터는 이 타입으로 간주한다"는 경계선을 만들고 싶을 때 유용합니다.
assertion function은 반드시 실패 흐름을 끊어야 한다
중요한 점은 assertion function 내부에서 검증 실패 시 반드시 이후 흐름을 끊어야 한다는 것입니다. 보통 throw new Error(...)를 사용합니다.
function assertUser(value: unknown): asserts value is User {
if (typeof value !== "object" || value === null) {
throw new Error("Invalid user");
}
}
만약 검증에 실패했는데도 함수가 정상 종료되면 문제가 생깁니다. TypeScript는 함수 선언을 믿고 호출 이후의 값을 User로 좁히기 때문입니다.
function assertUser(value: unknown): asserts value is User {
// 아무 검증도 하지 않음
}
이런 함수도 TypeScript 입장에서는 유효합니다. 컴파일러는 asserts value is User라는 선언을 신뢰하지만, 함수 내부 검증이 정말 정확한지는 증명하지 못합니다.
즉, assertion function은 타입 시스템과 런타임 검증 사이의 계약입니다. 계약을 잘못 작성하면 컴파일은 통과하지만 런타임 안정성은 깨질 수 있습니다.
실무에서는 외부 데이터 경계에서 유용하다
실무에서 assertion function은 외부 데이터를 내부 타입으로 들여오는 경계에서 특히 유용합니다.
예를 들어 API 응답을 처음에는 unknown으로 두고, 검증 함수를 통과한 뒤에만 내부 타입으로 사용하는 방식입니다.
type UserRole = "admin" | "editor" | "viewer";
type User = {
id: string;
role: UserRole;
};
function assertUser(value: unknown): asserts value is User {
if (
typeof value !== "object" ||
value === null ||
!("id" in value) ||
!("role" in value)
) {
throw new Error("Invalid user");
}
const user = value as { id: unknown; role: unknown };
if (typeof user.id !== "string") {
throw new Error("Invalid user id");
}
if (
user.role !== "admin" &&
user.role !== "editor" &&
user.role !== "viewer"
) {
throw new Error("Invalid user role");
}
}
이제 외부 응답값을 바로 User라고 믿지 않고, 먼저 런타임 검증을 통과시킬 수 있습니다.
const responseData: unknown = {
id: "u_123",
role: "editor",
};
assertUser(responseData);
console.log(responseData.id.toUpperCase());
console.log(responseData.role);
assertUser(responseData)가 통과한 뒤에는 responseData가 User로 좁혀지기 때문에, 별도의 if 블록 없이 id와 role에 접근할 수 있습니다.
코드 리뷰에서 봐야 할 지점
assertion function은 타입 가드보다 더 강한 계약입니다. 호출 이후 코드 흐름 전체에 영향을 주기 때문입니다.
그래서 이런 함수를 볼 때는 함수 이름보다 내부 검증의 완성도를 먼저 확인해야 합니다.
필수 필드를 모두 확인하는지, 문자열 리터럴 유니온을 빠짐없이 검사하는지, 중첩 객체나 배열 구조까지 필요한 만큼 검증하는지 봐야 합니다. 예를 들어 role이 "admin" | "editor" | "viewer"라면 단순히 문자열인지 확인하는 것만으로는 부족합니다. 실제 허용된 값 중 하나인지까지 검사해야 합니다.
특히 문자열 비교에서는 오타도 조심해야 합니다. "editor"를 "editors"처럼 잘못 적어도 TypeScript가 런타임 검증 로직의 의도를 완벽히 잡아주지는 못합니다.
정리
value is User는 boolean을 반환하는 타입 가드입니다. 사용부에서 if 같은 분기를 만들어야 타입이 좁혀집니다.
asserts value is User는 단언 함수입니다. 함수가 예외 없이 정상 종료되면, 호출 이후의 값이 해당 타입으로 좁혀집니다.
둘 다 외부 데이터를 안전하게 다루기 위한 도구지만, assertion function은 호출 이후 코드 흐름 전체에 영향을 주는 더 강한 계약입니다. 그래서 사용할 때는 내부 검증 로직을 신중하게 작성해야 합니다.
API 응답이나 사용자 입력처럼 신뢰할 수 없는 값을 내부 타입으로 들여오는 지점에서는 unknown으로 시작하고, 타입 가드나 assertion function으로 검증 경계를 세우는 습관이 좋습니다.
'TS' 카테고리의 다른 글
| TypeScript Utility Types 실무 기준: Partial, Pick, Omit, Record 제대로 쓰기 (0) | 2026.07.14 |
|---|---|
| TypeScript keyof와 T[K], 매핑 타입 한 번에 이해하기 (0) | 2026.07.13 |
| TypeScript 타입 가드: boolean과 type predicate 차이 (0) | 2026.07.10 |
| TypeScript never로 분기 누락 잡기: exhaustive checking (0) | 2026.07.09 |
