타입 가드와 타입 좁히기
typeof·instanceof·in·동등성 검사와 사용자 정의 타입 가드로 유니온 값의 실제 타입을 안전하게 좁힙니다.
타입스크립트는 정적 타입으로 코드 안정성을 높여 줍니다.
하지만 런타임에서는 변수의 실제 타입을 확인해 분기 처리해야 하는 상황이 자주 생깁니다.
예를 들어 string | number 같은 유니온 타입 변수는
실제 값이 문자열인지 숫자인지에 따라 실행 로직이 달라집니다.
이러한 상황에서 유용하게 사용되는 것이 바로 타입 가드(Type Guard)입니다.
타입 가드는 특정 스코프(코드 블록) 내에서 변수의 타입을 더욱 구체적인 타입으로 좁히는(Narrowing) 역할을 합니다.
이를 통해 우리는 런타임에 안전하게 해당 타입의 속성이나 메서드에 접근할 수 있게 됩니다.
타입 좁히기의 개념
unknown이나 union은 바로 고유 멤버에 접근할 수 없고, 조건문을 통과할 때마다 더 구체적인 타입이 된다.
- Wide input unknown 또
union 값 수신
- Guard typeof, instanceof
in, equality 검사
- Narrowed branch 조건 블록 안
더 구체적인 타입
- Safe use 좁힌 뒤에만 고유 속성
메서드 사용
- Custom guard 반복 조건
value is T 함수로 분리
| 점검 | 기준 |
|---|---|
| 타입 단언 | 검증 없이 컴파일러만 설득 |
| 타입 가드 | 런타임 조건으로 값의 형태를 증명 |
| 판단 | 외부 값은 가드로 증명한 뒤 사용 |
타입 좁히기 (Type Narrowing)는 타입스크립트 컴파일러가 코드의 특정 지점에서 변수 타입을 원래 선언보다 더 구체적으로 인식하는 과정입니다.
이는 주로 조건문(if, else if), 루프, 특정 연산자 사용을 통해 발생합니다.
타입 좁히기가 이루어지면 해당 코드 블록 안에서 변수는 좁혀진 타입의 속성과 메서드만 가진 것으로 간주되어 타입 안전성이 높아집니다.
function printId(id: string | number) {
// 처음에는 'id'의 타입이 'string | number'입니다.
console.log(`ID: ${id}`);
// id.toUpperCase(); // Error: 'string | number' 형식에 'toUpperCase' 속성이 없습니다.
// number 타입에는 이 메서드가 없기 때문입니다.
// 타입 가드를 사용하여 타입을 좁힙니다.
if (typeof id === 'string') {
// 이 if 블록 안에서 'id'의 타입은 'string'으로 좁혀집니다.
console.log(`대문자 ID: ${id.toUpperCase()}`);
} else {
// 이 else 블록 안에서 'id'의 타입은 'number'로 좁혀집니다.
console.log(`고정 소수점 ID: ${id.toFixed(2)}`);
}
}
printId("abc123DEF"); // ID: abc123DEF, 대문자 ID: ABC123DEF
printId(123.456); // ID: 123.456, 고정 소수점 ID: 123.46위 예시에서 typeof id === 'string' 조건문이 바로 타입 가드입니다.
이 조건이 참일 경우, 타입스크립트는 if 블록 내에서 id의 타입을 string으로 좁혀줍니다.
유니온 값은 런타임 검사를 통과한 분기 안에서 더 구체적인 타입이 되고, 해당 타입의 멤버만 안전하게 사용할 수 있습니다.
- 참typeof id === "string"
id는 string으로 좁혀지므로 toUpperCase()를 호출할 수 있습니다.
- 거짓else → number
string 가능성이 제거되어 id는 number가 되고 toFixed()를 호출할 수 있습니다.
- 합류분기 밖
두 경로가 다시 만나면 선언된 string | number 범위로 돌아가 공통 동작만 허용됩니다.
주요 타입 가드 종류
타입스크립트에서 기본적으로 제공하는 몇 가지 유용한 타입 가드들이 있습니다.
typeof 연산자
원시 타입(string, number, boolean, symbol, undefined, bigint)에 대한 타입을 확인하는 데 사용됩니다.
function printValue(value: string | number | boolean) {
if (typeof value === 'string') {
console.log(`문자열: ${value.length}`);
} else if (typeof value === 'number') {
console.log(`숫자: ${value * 2}`);
} else {
console.log(`불리언: ${!value}`);
}
}
printValue("hello"); // 문자열: 5
printValue(100); // 숫자: 200
printValue(true); // 불리언: falseinstanceof 연산자
클래스의 인스턴스인지 확인하는 데 사용됩니다.
특정 클래스의 인스턴스 또는 해당 클래스를 상속받은 인스턴스에 대해 타입을 좁힐 수 있습니다.
class Bird {
fly() { console.log('새가 날아갑니다.'); }
layEggs() { console.log('알을 낳습니다.'); }
}
class Fish {
swim() { console.log('물고기가 헤엄칩니다.'); }
layEggs() { console.log('알을 낳습니다.'); }
}
function move(animal: Bird | Fish) {
if (animal instanceof Bird) {
animal.fly(); // 'animal'은 Bird 타입으로 좁혀짐
} else {
animal.swim(); // 'animal'은 Fish 타입으로 좁혀짐
}
animal.layEggs(); // 'Bird'와 'Fish' 모두 layEggs 메서드를 가지므로 안전하게 호출 가능
}
move(new Bird()); // 새가 날아갑니다., 알을 낳습니다.
move(new Fish()); // 물고기가 헤엄칩니다., 알을 낳습니다.in 연산자
객체에 특정 속성(property)이 존재하는지 확인하는 데 사용됩니다.
유니온 타입으로 묶인 객체 타입에서 특정 속성의 유무로 타입을 구분할 때 유용합니다.
interface Car {
drive(): void;
}
interface Boat {
sail(): void;
}
function operateVehicle(vehicle: Car | Boat) {
if ('drive' in vehicle) {
vehicle.drive(); // 'vehicle'은 Car 타입으로 좁혀짐
} else {
vehicle.sail(); // 'vehicle'은 Boat 타입으로 좁혀짐
}
}
class MyCar implements Car { drive() { console.log('자동차 운전!'); } }
class MyBoat implements Boat { sail() { console.log('보트 항해!'); } }
operateVehicle(new MyCar()); // 자동차 운전!
operateVehicle(new MyBoat()); // 보트 항해!동등성 검사 (==, ===, !=, !==)
null 또는 undefined와 같은 리터럴 값과의 동등성 검사를 통해 타입을 좁힐 수 있습니다.
특히 선택적 속성이나 null이 될 수 있는 타입(string | null)에서 유용합니다.
function greetUser(user: { name: string; email?: string | null }) {
console.log(`안녕하세요, ${user.name}님!`);
if (user.email) { // user.email이 null 또는 undefined가 아닐 때
// 이 블록 안에서 user.email의 타입은 string으로 좁혀집니다.
console.log(`이메일: ${user.email.toLowerCase()}`);
} else {
console.log("이메일 정보가 없습니다.");
}
}
greetUser({ name: "Alice" }); // 안녕하세요, Alice님!, 이메일 정보가 없습니다.
greetUser({ name: "Bob", email: "bob@example.com" }); // 안녕하세요, Bob님!, 이메일: bob@example.com
greetUser({ name: "Charlie", email: null }); // 안녕하세요, Charlie님!, 이메일 정보가 없습니다.사용자 정의 타입 가드
기본 제공되는 타입 가드로는 충분하지 않거나, 더 복잡한 로직으로 타입을 좁히고 싶을 때 사용자 정의 타입 가드(User-Defined Type Guards)를 직접 만들 수 있습니다.
사용자 정의 타입 가드 함수는 반환 타입으로 parameterName is Type 형태의 타입 프레디케이트(Type Predicate)를 가집니다.
interface AdminUser {
id: number;
role: 'admin';
manageUsers(): void;
}
interface GeneralUser {
id: number;
role: 'general';
viewProfile(): void;
}
type User = AdminUser | GeneralUser;
// isAdminUser는 사용자 정의 타입 가드 함수입니다.
// user is AdminUser: 이 함수가 true를 반환하면 user의 타입이 AdminUser임을 보증합니다.
function isAdminUser(user: User): user is AdminUser {
return (user as AdminUser).role === 'admin';
}
function handleUser(user: User) {
if (isAdminUser(user)) {
// 이 블록 안에서 user는 AdminUser 타입으로 좁혀집니다.
console.log("관리자 권한으로 로그인했습니다.");
user.manageUsers();
} else {
// 이 블록 안에서 user는 GeneralUser 타입으로 좁혀집니다.
console.log("일반 사용자 권한으로 로그인했습니다.");
user.viewProfile();
}
}
const admin: AdminUser = { id: 1, role: 'admin', manageUsers: () => console.log("사용자 관리") };
const general: GeneralUser = { id: 2, role: 'general', viewProfile: () => console.log("프로필 보기") };
handleUser(admin); // 관리자 권한으로 로그인했습니다., 사용자 관리
handleUser(general); // 일반 사용자 권한으로 로그인했습니다., 프로필 보기user is AdminUser는 함수가 true를 반환할 때, 첫 번째 매개변수 user가 AdminUser 타입임을 타입스크립트 컴파일러에게 알려줍니다.
이처럼 사용자 정의 타입 가드를 사용하면 런타임 로직과 타입 검사를 긴밀하게 통합할 수 있습니다.
아래 다이어그램은 사용자 정의 타입 가드가 런타임 검사와 호출부의 타입 좁히기를 어떻게 연결하는지 보여줍니다.
사용자 정의 타입 가드는 런타임 조건을 통과한 값을 호출부에서 더 좁은 타입으로 쓰게 해 준다.
- 타입 프레디케이트
user is AdminUser 가 true 분기에서의 타입을 알려 준다.
- 런타임 증거
role === "admin" 처럼 실제 값으로 확인 가능한 조건을 둔다.
- else 분기
true가 아니면 남은 타입인 GeneralUser 로 좁혀진다.
- 반복 조건 캡슐화
여러 함수에서 같은 분기 검사를 안전하게 재사용한다.
타입 가드는 런타임 검사와 컴파일러의 타입 추론을 함께 설계해야 효과가 큽니다.
아래 표는 값의 형태에 따라 어떤 좁히기 방식을 먼저 선택할지 정리합니다.
값의 정체를 확인하는 방식에 맞춰 좁히기 도구를 고르면 분기 안의 타입이 안정적으로 따라옵니다.
- typeof원시값
문자열, 숫자, 불리언처럼 런타임 타입 이름이 바로 보이는 값에 씁니다.
- instanceof클래스 인스턴스
Date, Error, 직접 만든 클래스처럼 생성자 관계가 있는 값을 확인합니다.
- in속성 존재
유니온 멤버가 서로 다른 필드 이름을 가질 때 분기 기준으로 삼습니다.
- is T도메인 규칙
여러 조건을 묶어야 하면 사용자 정의 가드로 검사와 타입을 연결합니다.
타입 좁히기가 잘못 설계되면 분기 안의 타입은 좁아져 보여도 실제 런타임 값과 어긋날 수 있습니다.
아래 다이어그램은 그런 위험 신호와 대응 방식을 정리합니다.
컴파일러가 이해할 수 있는 증거를 순서대로 만들고 검사가 끝난 범위 안에서만 좁힌 타입을 사용한다.
- 1unknown 입력
외부 값은 아직 어떤 속성도 믿지 않는다
- 2구조 검사
typeof·Array.isArray·in으로 후보를 줄인다
- 3구별 필드
kind 같은 literal 필드로 한 변형을 확정한다
- 4exhaustive
never로 빠진 후보가 없는지 확인한다
타입 가드와 타입 좁히기는 타입스크립트의 유니온 타입과 함께 강력한 시너지를 발휘하는 기능입니다.
런타임에 동적으로 변수의 타입을 확인하고 그에 따라 적절한 타입의 멤버에 안전하게 접근함으로써, 런타임 오류를 줄이고 코드의 견고성을 크게 향상시킬 수 있습니다.
typeof, instanceof, in 연산자와 더불어 사용자 정의 타입 가드를 적절히 활용하는 것이 중요합니다.
다음 다이어그램은 타입 가드와 타입 좁히기를 검사 방식별로 정리한 복습표입니다.
typeof, instanceof, in, 동등성 검사, 사용자 정의 가드를 흐름별로 고르면 코드 안에서 타입이 점점 구체화됩니다.
- 1원시값 확인
typeof string, number, boolean처럼 런타임 종류를 바로 확인할 수 있는 값에 씁니다. typeof value
- 2클래스 확인
instanceof Date나 Error처럼 생성자 기반 인스턴스 여부를 판별할 때 사용합니다. value instanceof Date
- 3속성 존재
in 연산자 객체 유니온에서 특정 속성이 있는 가지로 안전하게 좁힙니다. "kind" in value
- 4가드 재사용
타입 predicate 반복되는 판별 로직은 value is Type 형태의 함수로 분리합니다. value is User
이 다이어그램은 타입 가드와 타입 좁히기 학습을 선언 위치, 타입 좁히기, 재사용 경계 순서로 마무리합니다.
유니온이나 unknown 값은 실행 중 확인을 거친 뒤에야 특정 속성과 메서드 접근 범위가 좁아집니다.
- 원시값 구분
typeof string, number, boolean 같은 기본 타입은 typeof 분기로 안전하게 좁힙니다. typeof value
- 속성 존재 확인
in 객체 유니온에서는 특정 속성이 있는지 확인해 가능한 타입을 구분합니다. "kind" in x
- 상태별 모델
판별 속성 공통 kind 값을 기준으로 성공, 실패, 로딩 같은 상태를 명확히 나눕니다. kind
- 검사 함수 재사용
사용자 가드 복잡한 확인 로직은 value is Type 반환 형태로 묶어 여러 곳에서 재사용합니다. isUser
아래 다이어그램은 타입 가드와 타입 좁히기를 값 확인 방식별로 정리합니다.
타입 좁히기는 넓게 선언된 값에서 현재 분기에 안전하게 사용할 수 있는 타입만 남기는 과정입니다.
- typeof원시 타입 확인
typeof string, number, boolean 같은 원시값 분기에 맞춰 런타임 타입을 좁힙니다. typeof value === "string"
- instanceof클래스 인스턴스 확인
instanceof Date, Error, 사용자 클래스처럼 생성자 기반 객체를 좁힐 때 씁니다. err instanceof Error
- in 연산자속성 존재 확인
in 연산자 객체 유니온에서 특정 속성이 있는 분기를 골라낼 수 있습니다. "swim" in animal
- 사용자 가드도메인 규칙 캡슐화
사용자 가드 반복되는 확인 로직은 value is Type 반환 함수로 이름 붙입니다. value is User
아래 다이어그램은 타입 가드와 타입 좁히기를 실제 코드에 적용하기 전에 설정 위치, 타입 해석, 빌드 결과를 확인합니다.