타입 가드
1. 타입 가드
타입 가드를 사용하면 특정 스코프 내에서 타입을 보장할 수 있습니다. "이 객체가 특정 타입인지 확인하고, 맞다면 TypeScript에게 그 사실을 알려주는" 방법이라고 생각하시면 됩니다.
1.1 typeof 타입 가드
typeof는 JavaScript의 typeof처럼 Array를 Object로 반환합니다. 따라서 객체를 취급할 때에는 주의해야 합니다.
function processValue(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string으로 타입 보장
}
return value.toFixed(2); // number로 타입 보장
}
typeof는 javascript와 typescript 모두 연산이 정확하지 않기 때문에 아래와 같이 별도의 사용자 정의 함수를 만들어 사용하는 것을 권합니다. javascript에서 사용 가능한 코드입니다.
function typeCheck(value) {
const return_value = Object.prototype.toString.call(value);
const type = return_value.substring(
return_value.indexOf(" ") + 1,
return_value.indexOf("]")
);
return type.toLowerCase();
}
타입스크립트에서는 아래와 같이 사용할 수 있습니다.
function typeCheck(value: any): string {
const return_value = Object.prototype.toString.call(value);
const type = return_value.substring(
return_value.indexOf(" ") + 1,
return_value.indexOf("]")
);
return type.toLowerCase();
}
console.log(typeCheck([]));
console.log(typeCheck(null));
1.2 instanceof 타입 가드
class Dog {
bark() { return "Woof!"; }
}
class Cat {
meow() { return "Meow!"; }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
return animal.bark();
}
return animal.meow();
}
const dog = new Dog();
const cat = new Cat();
console.log(makeSound(dog)); // Woof!
console.log(makeSound(cat)); // Meow!
1.3 사용자 정의 타입 가드
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
}
const fish = {
swim: () => console.log("물고기가 헤엄칩니다.")
};
const bird = {
fly: () => console.log("새가 날아갑니다.")
};
move(fish); // "물고기가 헤엄칩니다 🐠"
move(bird); // "새가 날아갑니다 🐦"
pet is Fish는 TypeScript의 타입 술어(Type Predicate)로, 함수가 true를 반환할 때 해당 매개변수가 Fish 타입이라는 것을 TypeScript에게 알려주는 역할을 합니다. 쉽게 말해서, 이 조건이 참이면 pet은 Fish 타입이다라고 TypeScript의 타입 시스템에게 알려주는 것입니다.
(pet as Fish).swim !== undefined는 타입 단언(Type Assertion)을 사용한 코드입니다. pet 객체를 Fish 타입이라고 가정(타입 단언)하고, swim이라는 메서드가 존재하는지 확인합니다. 만약 swim 메서드가 존재한다면 true를 반환하여 이 객체가 Fish 타입이라고 판단합니다. 즉, "물고기라면 반드시 가지고 있어야 할 swim 메서드가 있는지 확인"하는 것입니다.
1.4 in 연산자 타입 가드
in 연산자는 객체에 특정 속성이 존재하는지 확인할 때 사용합니다. 타입 단언보다 더 간단하고 안전하게 타입을 좁힐 수 있습니다.
interface Developer {
name: string;
code(): void;
}
interface Designer {
name: string;
design(): void;
}
function work(person: Developer | Designer) {
if ('code' in person) {
person.code(); // Developer로 타입이 좁혀짐
} else {
person.design(); // Designer로 타입이 좁혀짐
}
}
const dev: Developer = {
name: "철수",
code: () => console.log("코드를 작성합니다.")
};
const designer: Designer = {
name: "영희",
design: () => console.log("디자인을 합니다.")
};
work(dev); // "코드를 작성합니다."
work(designer); // "디자인을 합니다."
in 연산자는 배열 타입을 구분할 때도 유용합니다.
function processData(data: string | string[]) {
if (Array.isArray(data)) {
console.log(`배열 길이: ${data.length}`);
data.forEach(item => console.log(item)); // string[]로 타입 보장
} else {
console.log(`문자열 길이: ${data.length}`); // string으로 타입 보장
}
}
processData("안녕하세요");
processData(["사과", "바나나", "오렌지"]);
null이나 undefined를 체크할 때도 타입 가드가 동작합니다.
function printLength(text: string | null | undefined) {
// 방법 1: truthy 체크
if (text) {
console.log(text.length); // string으로 타입 보장
}
// 방법 2: null 명시적 체크
if (text !== null && text !== undefined) {
console.log(text.length);
}
// 방법 3: nullish 체크 (null과 undefined 모두 체크)
if (text != null) {
console.log(text.length);
}
}
function processUser(user: { name: string; age?: number }) {
if (user.age !== undefined) {
console.log(`내년 나이: ${user.age + 1}세`); // number로 타입 보장
}
}
processUser({ name: 'Sunny', age: 3000 })
2. 실전 타입 가드 패턴
2.1 Discriminated Union 패턴
Discriminated Union은 공통 속성(discriminant)을 사용해 타입을 구분하는 패턴입니다. 유니온 타입의 각 멤버를 확실하게 구별할 수 있도록 공통 판별자 속성을 두는 패턴이며, 주로 리터럴 타입으로 처리합니다. API 응답 처리 등 실무에서 자주 사용됩니다.
// API 응답 타입 정의
interface SuccessResponse {
status: 'success'; // 공통 판별자: 고유한 리터럴 값
data: {
userId: number;
username: string;
};
}
interface ErrorResponse {
status: 'error'; // 공통 판별자: 고유한 리터럴 값
error: {
code: number;
message: string;
};
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
// status 필드로 타입 자동 판별
if (response.status === 'success') {
console.log(`사용자: ${response.data.username}`);
// response는 자동으로 SuccessResponse 타입
} else {
console.log(`에러 ${response.error.code}: ${response.error.message}`);
// response는 자동으로 ErrorResponse 타입
}
}
// 사용 예시
const success: ApiResponse = {
status: 'success',
data: { userId: 1, username: 'john' }
};
const error: ApiResponse = {
status: 'error',
error: { code: 404, message: '사용자를 찾을 수 없습니다' }
};
handleResponse(success); // "사용자: john"
handleResponse(error); // "에러 404: 사용자를 찾을 수 없습니다"
2.2 switch문 활용
여러 타입을 처리해야 할 때 switch문을 사용하면 코드가 더 깔끔해집니다.
type NetworkStatus = 'online' | 'offline' | 'connecting';
interface NetworkState {
status: NetworkStatus;
lastUpdate: Date;
}
function getStatusMessage(state: NetworkState): string {
switch (state.status) {
case 'online':
return '✓ 연결됨';
case 'offline':
return '✗ 연결 끊김';
case 'connecting':
return '⟳ 연결 중...';
default:
// 모든 케이스를 처리했는지 컴파일 타임에 확인
const exhaustiveCheck: never = state.status;
throw new Error(`처리되지 않은 상태: ${exhaustiveCheck}`);
}
}
const state: NetworkState = {
status: 'online',
lastUpdate: new Date()
};
console.log(getStatusMessage(state)); // "✓ 연결됨"
never 타입을 사용하면 모든 경우를 처리했는지 TypeScript가 검사해줍니다. 만약 새로운 상태가 추가되면 컴파일 에러가 발생해 놓친 케이스를 바로 알 수 있습니다.
2.3 복합 조건 타입 가드
여러 조건을 결합해서 더 복잡한 타입 가드를 만들 수 있습니다.
interface Admin {
role: 'admin';
permissions: string[];
department: string;
}
interface User {
role: 'user';
email: string;
verified: boolean;
}
interface Guest {
role: 'guest';
sessionId: string;
}
type Person = Admin | User | Guest;
// 복합 조건으로 권한 체크
function canEditContent(person: Person): boolean {
if (person.role === 'admin' && person.permissions.includes('edit')) {
return true;
}
return false;
}
// 타입별로 다른 정보 추출
function getContactInfo(person: Person): string | null {
if (person.role === 'user') {
return person.verified ? person.email : null;
}
if (person.role === 'admin') {
return `${person.department} 부서 관리자`;
}
return null;
}
// 사용 예시
const admin: Admin = {
role: 'admin',
permissions: ['edit', 'delete'],
department: '개발'
};
const user: User = {
role: 'user',
email: 'user@example.com',
verified: true
};
console.log(canEditContent(admin)); // true
console.log(getContactInfo(user)); // "user@example.com"
console.log(getContactInfo(admin)); // "개발 부서 관리자"
3. 타입 가드 주의사항
3.1 타입 단언 vs 타입 가드 비교
타입 단언과 타입 가드는 비슷해 보이지만 안전성에 큰 차이가 있습니다.
// ❌ 타입 단언 - 런타임 안전성 없음
function processInputBad(input: unknown) {
const str = input as string; // 위험! 컴파일은 되지만 런타임 에러 가능
return str.toUpperCase();
}
// 런타임 에러 발생!
// processInputBad(123); // TypeError: str.toUpperCase is not a function
// ✅ 타입 가드 - 런타임 안전성 보장
function processInputGood(input: unknown) {
if (typeof input === 'string') {
return input.toUpperCase(); // 안전!
}
throw new Error('문자열이 아닙니다');
}
// 안전하게 에러 처리
try {
processInputGood(123);
} catch (e) {
console.log('에러가 발생했습니다'); // 에러가 발생했습니다
}
타입 단언을 사용해야 하는 경우:
- 외부 라이브러리의 타입이 부정확할 때
- TypeScript보다 개발자가 타입을 더 정확히 알고 있을 때
- DOM 요소를 다룰 때 (예:
document.getElementById('app') as HTMLDivElement)
타입 가드를 사용해야 하는 경우:
- 런타임에 실제로 타입을 확인해야 할 때
- 사용자 입력이나 외부 데이터를 처리할 때
- 타입 안전성이 중요한 비즈니스 로직
3.2 옵셔널 체이닝과 함께 사용하기
중첩된 객체를 안전하게 다루려면 타입 가드와 옵셔널 체이닝을 함께 사용합니다.
interface Product {
id: number;
name: string;
details?: {
description?: string;
specs?: {
weight?: number;
dimensions?: string;
};
};
}
// ❌ 타입 가드 없이 접근
function getWeightBad(product: Product): number {
return product.details.specs.weight; // 에러! undefined일 수 있음
}
// ✅ 옵셔널 체이닝 사용
function getWeightWithChaining(product: Product): number | undefined {
return product.details?.specs?.weight; // 안전하지만 undefined 가능
}
// ✅ 타입 가드와 함께 사용 (가장 안전)
function getWeightSafe(product: Product): number | null {
if (product.details?.specs?.weight !== undefined) {
return product.details.specs.weight; // number로 타입 보장
}
return null;
}
// 사용 예시
const product1: Product = {
id: 1,
name: '노트북',
details: {
specs: {
weight: 1.5
}
}
};
const product2: Product = {
id: 2,
name: '키보드'
};
console.log(getWeightSafe(product1)); // 1.5
console.log(getWeightSafe(product2)); // null
옵셔널 체이닝(?.)을 사용하면 중간에 null이나 undefined가 있어도 에러가 발생하지 않고 안전하게 접근할 수 있습니다.
interface Company {
name: string;
address?: {
city?: string;
zipCode?: string;
};
}
function getCityName(company: Company): string {
// 타입 가드로 안전하게 처리
if (company.address?.city) {
return company.address.city.toUpperCase(); // string으로 보장
}
return '주소 정보 없음';
}
const company1: Company = {
name: '위니브',
address: {
city: '제주'
}
};
const company2: Company = {
name: '테스트회사'
};
console.log(getCityName(company1)); // "제주"
console.log(getCityName(company2)); // "주소 정보 없음"
4. 연습문제
- 다음과 같이 문자열 또는 숫자를 받아서 해당 값의 길이를 반환하는 함수를 작성하세요.
- 문자열이 들어오면 문자열의 길이를 반환
- 숫자가 들어오면 숫자를 문자열로 변환한 후 그 길이를 반환
- 다음과 같이 두 가지 타입의 동물 객체가 있습니다. 각 동물의 특성에 맞게 소리를 출력하는 함수를 작성하세요.
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
bark() {
return "멍멍!";
}
}
class Cat {
name: string;
constructor(name: string) {
this.name = name;
}
meow() {
return "야옹!";
}
}
// 여기에 makeSound 함수를 작성하세요.
// Dog나 Cat을 받아서 각각 알맞은 소리를 반환해야 합니다.
- 다음 코드에 타입 가드를 추가해보세요.
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
// 도형의 면적을 계산하는 함수를 작성하세요.
function calculateArea(shape: Shape): number {
// 여기에 구현
}
5. 연습문제 정답
- 문자열 또는 숫자를 받아서 해당 값의 길이를 반환하는 함수
function getLength(value: string | number): number {
if (typeof value === "string") {
return value.length;
}
return String(value).length;
}
// 사용 예시
console.log(getLength("hello")); // 5
console.log(getLength(12345)); // 5
- 각 동물의 특성에 맞게 소리를 출력하는 함수
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
bark() {
return "멍멍!";
}
}
class Cat {
name: string;
constructor(name: string) {
this.name = name;
}
meow() {
return "야옹!";
}
}
function makeSound(animal: Dog | Cat): string {
if (animal instanceof Dog) {
return `${animal.name}이(가) ${animal.bark()}`;
}
return `${animal.name}이(가) ${animal.meow()}`;
}
// 사용 예시
const dog = new Dog("멍멍이");
const cat = new Cat("야옹이");
console.log(makeSound(dog)); // "멍멍이가 멍멍!"
console.log(makeSound(cat)); // "야옹이가 야옹!"
- 도형의 면적을 계산하는 함수 (타입 가드 사용)
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
// 타입 가드 함수들
function isSquare(shape: Shape): shape is Square {
return shape.kind === "square";
}
function isRectangle(shape: Shape): shape is Rectangle {
return shape.kind === "rectangle";
}
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}
function calculateArea(shape: Shape): number {
if (isSquare(shape)) {
return shape.size * shape.size;
}
if (isRectangle(shape)) {
return shape.width * shape.height;
}
if (isCircle(shape)) {
return Math.PI * shape.radius * shape.radius;
}
// 모든 케이스를 처리했지만, TypeScript의 타입 체크를 위해 필요
const unreachable: never = shape;
throw new Error(`Unhandled shape type: ${unreachable}`);
}
// 사용 예시
const square: Square = { kind: "square", size: 5 };
const rectangle: Rectangle = { kind: "rectangle", width: 4, height: 6 };
const circle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(square)); // 25
console.log(calculateArea(rectangle)); // 24
console.log(calculateArea(circle)); // 약 28.27