타입 좁히기(Type-Narrowing)
referencetypescripttype-narrowingtype-guard
특정 상황에서 타입의 범위를 더 좁고 명확한 타입으로 한정하는 과정
타입 좁히기는 조건문 등을 이용해 넓은 타입에서 좁은 타입으로 변환하는 과정이다.
조건문과 함께 사용해 타입을 좁히는 표현들을 타입 가드(Type Guard)라고 한다.
아래 3가지가 대표적인 타입 가드다.
🎯 실무에서 이런 상황에 유용하다
- API 응답 처리: 서버에서 받은 데이터가 성공/실패에 따라 다른 타입일 때
- 폼 데이터 검증: 사용자 입력값이 문자열인지 숫자인지 확인할 때
- 이벤트 핸들링: 다양한 이벤트 타입(MouseEvent, KeyboardEvent 등)을 구분할 때
- 유니온 타입 처리: 여러 타입을 받을 수 있는 함수에서 각 타입별로 다른 로직을 실행할 때
💡 사용 예시
1. typeof를 이용한 타입 좁히기
function handleValue(value: string | number) {
if (typeof value === "string") {
// value는 이제 string 타입으로 좁혀짐
console.log(value.toUpperCase());
} else {
// value는 이제 number 타입으로 좁혀짐
console.log(value.toFixed(2));
}
}
만약 JS 기본 클래스들도 typeof로 하면 어떨까?
function handleClass(value: string | number | Date | null) {
if (typeof value === "object") {
console.log(value.getTime()); // ❌ Error: value는 'null'일 수 있습니다.
}
}
typeof 연산자는 null 값에 해도 "object"를 반환하기 때문에 위험 할 수 있다.
2. instanceof를 이용한 타입 좁히기
function handleClass(value: string | number | Date | null) {
if (value instanceof Date) {
console.log(value.getTime());
}
}
이때 instanceof를 사용하면 된다. 하지만, instanceof는 내장 클래스나 직접 만든 클래스에만 사용할 수 있다. 즉, 직접 만든 타입을 체크할 수 없는 단점이 있다.
3. in을 이용한 타입 좁히기
type Person = {
name: string;
age: number;
};
function processPerson(person: Person | null) {
if (person && "age" in person) {
// person은 이제 Person 타입으로 좁혀짐
console.log(person.age);
}
}
in은 객체가 특정 프로퍼티를 가지고 있는지 확인하는 연산자이다. 대신 null일 때는 안전하게 처리해야 하기 때문에 앞에 null 체크를 해주는 것이 좋다.
💡 서로소 유니온 타입을 이용해 타입 좁히기
type User = {
name: string;
age: number;
};
type Admin = {
name: string;
role: string;
};
type Person = User | Admin;
function processPerson(person: Person) {
if ("age" in person) {
console.log(person.name, person.age);
} else {
console.log(person.name, person.role);
}
}
이런식으로 하면 코드를 짠 사람은 이해하지만, 코드를 읽는 사람은 타입까지 읽어야해서 가독성이 떨어진다. 이때, 서로소 유니온 타입을 사용한다면 훨씬 가독성 있는 코드를 짤 수 있다.
type User = {
type: "user";
name: string;
age: number;
};
type Admin = {
type: "admin";
role: string;
};
type Person = User | Admin;
function processPerson(person: Person) {
if (person.type === "user") { // person은 User 타입으로 좁혀짐
console.log(person.name, person.age);
} else { // person은 Admin 타입으로 좁혀짐
console.log(person.name, person.role);
}
}
함수만 봐도 딱 알 수 있게 된다.
🎯 실무에서 사용해볼만한 코드
type AsyncResponse = {
status: 'success' | 'error' | 'loading';
error?: string;
data?: string;
};
function processResponse(response: AsyncResponse) {
switch (response.status) {
case 'success':
console.log(response.data); // data가 있는지 모름. 따라서 ?나 !를 사용해야만함.
break;
case 'error':
console.log(response.error); // error가 있는지 모름. 따라서 ?나 !를 사용해야만함.
break;
case 'loading':
console.log('Loading...');
break;
}
}
위의 코드의 타입을 좀 더 잘게 쪼갠다면,
type SuccessResponse = {
status: 'success';
data: string;
};
type ErrorResponse = {
status: 'error';
error: string;
};
type LoadingResponse = {
status: 'loading';
};
type AsyncResponse = SuccessResponse | ErrorResponse | LoadingResponse;
function processResponse(response: AsyncResponse) {
switch (response.status) {
case 'success':
console.log(response.data);
break;
case 'error':
console.log(response.error);
break;
case 'loading':
console.log('Loading...');
break;
}
}

