
TypeScript를 쓰다 보면 unknown 타입의 값을 다뤄야 할 때가 있습니다. API 응답, 외부 JSON, 사용자 입력처럼 아직 믿을 수 없는 값이 대표적입니다.
이때 필요한 것이 타입 가드(Type Guard)입니다. 타입 가드는 런타임에서 값을 검사하고, 그 결과를 TypeScript의 타입 좁히기와 연결해 주는 방식입니다.
다만 중요한 차이가 있습니다. 단순히 boolean을 반환하는 함수는 타입을 좁혀주지 않습니다. 타입을 좁히려면 반환 타입에 value is SomeType 형태의 type predicate를 써야 합니다.
TypeScript는 조건문 안에서 타입을 좁힌다
TypeScript는 typeof, instanceof, in, 동등 비교 같은 조건을 보고 타입을 좁혀줍니다.
function printUpper(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase());
}
}
위 코드에서 value는 처음에는 unknown입니다. 하지만 typeof value === "string" 조건문 안에서는 TypeScript가 value를 string으로 판단합니다.
그래서 toUpperCase()를 안전하게 호출할 수 있습니다.
boolean을 반환하면 타입이 좁혀지지 않는다
문제는 이 검사를 함수로 분리했을 때 생깁니다.
function isString(value: unknown): boolean {
return typeof value === "string";
}
function printUpper(value: unknown) {
if (isString(value)) {
value.toUpperCase();
// Error:
// 'value' is of type 'unknown'.
}
}
사람이 보기에는 isString(value)가 true라면 value는 문자열입니다.
하지만 TypeScript 입장에서는 다릅니다. isString은 그냥 boolean을 반환하는 함수일 뿐입니다. 이 함수가 어떤 값을 어떤 타입으로 검증했는지 컴파일러는 알 수 없습니다.
그래서 if문 안에서도 value는 여전히 unknown입니다.

type predicate를 쓰면 타입이 좁혀진다
이럴 때 반환 타입을 boolean이 아니라 type predicate로 작성합니다.
function isString(value: unknown): value is string {
return typeof value === "string";
}
function printUpper(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
}
}
printUpper("hello"); // HELLO
핵심은 이 부분입니다.
value is string
이 선언은 TypeScript에게 “이 함수가 true를 반환하는 분기에서는 value를 string으로 봐도 된다”는 정보를 전달합니다.
덕분에 if (isString(value)) 안에서 value는 unknown이 아니라 string으로 좁혀집니다.
객체 타입 가드 예시
실무에서는 문자열 하나보다 객체를 검증하는 경우가 더 많습니다.
예를 들어 다음과 같은 User 타입이 있다고 가정해 보겠습니다.
type User = {
id: string;
name: string;
role: "admin" | "member" | "guest";
};
외부에서 들어온 값을 User로 다루려면 먼저 객체인지, 필요한 필드가 있는지, 각 필드의 타입이 맞는지 확인해야 합니다.
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Record<string, unknown>;
return (
typeof candidate.id === "string" &&
typeof candidate.name === "string" &&
(candidate.role === "admin" ||
candidate.role === "member" ||
candidate.role === "guest")
);
}
이제 isUser(raw)가 true인 분기에서는 raw를 User로 사용할 수 있습니다.
const raw: unknown = JSON.parse(
'{"id":"u_1","name":"Hojeong","role":"member"}',
);
if (isUser(raw)) {
console.log(raw.name.toUpperCase());
console.log(raw.role);
} else {
console.error("Invalid user payload");
}
여기서 중요한 점은 필드 존재 여부만 보는 것이 아니라 필드의 타입까지 확인해야 한다는 것입니다. id라는 키가 있다고 해서 그 값이 반드시 문자열인 것은 아니기 때문입니다.
타입 가드는 컴파일러를 속일 수도 있다
type predicate는 강력하지만, TypeScript가 함수 내부 구현의 정확성을 완전히 검증해 주지는 않습니다.
예를 들어 아래 코드는 잘못된 타입 가드입니다.
function isString(value: unknown): value is string {
return typeof value === "number";
}
function printUpper(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
}
}
printUpper(123);
함수 선언은 value is string이라고 말하지만, 실제 구현은 number를 통과시키고 있습니다.
TypeScript는 type predicate 선언을 믿습니다. 그래서 컴파일 단계에서는 통과할 수 있지만, 런타임에서는 toUpperCase()를 호출하는 순간 문제가 생깁니다.
즉, 타입 가드는 타입 안정성을 높여주는 도구이지만 구현이 틀리면 오히려 위험해질 수 있습니다.
실무에서 타입 가드를 쓸 때 기준
타입 가드는 다음 상황에서 유용합니다.
unknown값을 받은 뒤 내부 값에 접근해야 할 때- 타입 체크 함수를 분리하고 싶을 때
- 조건문 안에서 TypeScript의 타입 좁히기를 유지하고 싶을 때
- 간단한 객체 구조를 직접 검증할 때
반대로 검증해야 할 데이터 구조가 복잡하다면 직접 타입 가드를 계속 늘리는 방식은 부담이 됩니다.
API 응답, 폼 입력, 외부 JSON처럼 신뢰할 수 없는 데이터가 많다면 zod 같은 런타임 스키마 검증 라이브러리를 함께 쓰는 편이 더 안전하고 유지보수하기 쉽습니다.
정리
TypeScript 타입 가드의 핵심은 단순한 런타임 검사가 아니라, 그 검사 결과를 컴파일러의 타입 좁히기와 연결하는 데 있습니다.
boolean을 반환하는 함수는 참과 거짓만 알려줍니다. 반면 value is Type 형태의 type predicate는 참인 분기에서 해당 값의 타입을 좁혀줍니다.
다만 TypeScript는 타입 가드 구현이 실제로 올바른지 완전히 보장하지 않습니다. 따라서 타입 가드는 짧고 명확하게 작성하고, 객체를 검증할 때는 필드 존재 여부뿐 아니라 필드 타입까지 함께 확인하는 습관이 필요합니다.
'TS' 카테고리의 다른 글
| TypeScript Utility Types 실무 기준: Partial, Pick, Omit, Record 제대로 쓰기 (0) | 2026.07.14 |
|---|---|
| TypeScript keyof와 T[K], 매핑 타입 한 번에 이해하기 (0) | 2026.07.13 |
| TypeScript assertion function 쉽게 이해하기 (0) | 2026.07.11 |
| TypeScript never로 분기 누락 잡기: exhaustive checking (0) | 2026.07.09 |
