
타입스크립트는 정적으로 타입을 검사하지만, 코드 실행 중에 타입을 확인하고 그에 따라 다르게 동작해야 하는 경우가 많다. 그런 상황에서 타입 좁히기를 통해 타입 안정성을 유지하면서도 다양한 타입에 대한 처리를 할 수 있다.
💡 타입 좁히기 (Type Narrowing)란?
: 여러 가능한 타입 중 하나로 타입을 좁히는 과정이다. 즉, 코드 실행 중에 특정 값이 어떤 타입인지 확인하고, 그 타입에 맞는 로직을 적용하는 것
💡 타입 가드 (Type Guard)란?
: 런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능을 뜻한다. (타입좁히기를 구현하기 위한 수단 정도로 생각해도 좋다)
type User = {
name: string;
age?: number;
}
function printUserAge(user: User) {
if (user.age !== undefined) {
console.log(`User's age is ${user.age}`);
} else {
console.log("User's age is not provided");
}
}
printUserAge({ name: "abc"})
printUserAge({ name: "abc", age: 30})
위 코드는 옵셔널 속성(age?)을 타입 가드로 확인하는 예제이다.
옵셔널 속성은 객체에 존재하지 않을 수 있으므로, 접근하기 전에 반드시 확인이 필요하다.
user.age !== undefined를 통해 user.age가 undefined가 아니면 해당 속성이 존재한다고 간주하며, 타입을 number로 좁힌다.
typeof 타입가드function printValue(value: string | number) {
if (typeof value === 'string') {
console.log(`String: ${value.toUpperCase()}`);
} else {
console.log(`Number: ${value.toFixed(2)}`);
}
}
// 테스트 코드
printValue("Hello"); // String: HELLO
printValue(42); // Number: 42.00
위 코드는 typeof 연산자를 사용하여 변수의 타입을 확인하고, 그에 따라 분기 처리를 하고 있다.
typeof value === "string"이면 문자열 관련 메서드인 toUpperCase()를 호출하고, 그렇지 않으면 숫자 관련 메서드인 toFixed()를 호출한다.
instanceof 타입 가드import axios, { AxiosError } from 'axios';
async function fetchData(url: string) {
try {
const response = await axios.get(url);
console.log('Data:', response.data);
} catch (error) {
if (error instanceof AxiosError) {
// AxiosError인 경우
console.log('Axios error occurred')
} else {
// 일반 Error인 경우
console.log('General error occurred');
}
}
}
위 코드는 instanceof를 사용하여 특정 클래스의 인스턴스인지 확인하는 타입 가드 예제이다.
error instanceof AxiosError는 error 객체가 AxiosError 클래스에서 파생된 인스턴스인지를 검사하여, AxiosError인 경우에는 Axios와 관련된 오류로 처리하고, 그렇지 않으면 일반적인 Error로 처리한다.
in 타입가드type Dog = {
bark: () => void;
}
type Cat = {
meow: () => void;
}
function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark();
} else {
animal.meow();
}
}
const dog: Dog = { bark: () => console.log('Woof!') };
const cat: Cat = { meow: () => console.log('Meow!') };
makeSound(dog); // Woof!
makeSound(cat); // Meow!
위 코드는 객체에 특정 키가 존재하는지 확인하여 타입을 좁히는 타입 가드의 예제이다.
'bark' in animal은 animal 객체에 'bark'라는 키가 존재하는지 검사하여 존재하면 animal을 Dog 타입으로 간주한다.
따라서, bark 키가 있으면 Dog 타입으로 판단하여 bark 메서드를 호출하고 없으면 Cat 타입으로 판단하여 meow 메서드를 호출한다.
사용자 정의 타입가드란 참, 거짓을 반환하는 함수를 정의하여 이 함수를 이용해 사용자가 원하는 타입가드를 만들 수 있도록 도와주는 타입스크립트 문법이다.
type Dog = {
bark: () => void;
}
type Cat = {
meow: () => void;
}
// isDog 함수의 리턴값이 true 라면, animal 은 Dog다.
function isDog(animal: Dog | Cat): animal is Dog {
return "bark" in animal;
}
function makeSound(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark();
} else {
animal.meow();
}
}
const dog: Dog = { bark: () => console.log('Woof!') };
const cat: Cat = { meow: () => console.log('Meow!') };
makeSound(dog); // Woof!
makeSound(cat); // Meow!
위 코드는 is 키워드를 사용하여 타입을 좁히는 사용자 정의 타입가드의 예제이다.
animal.bark가 정의되어 있으면 true를 리턴하여 animal is Dog 즉, Dog 타입으로 판별한다. 함수의 반환값이 true면 Dog 타입, 그렇지 않으면 Cat 타입으로 처리된다.
const user: { id: number; name: string; email: string } = {
id: 1,
name: "Alice",
email: "alice@example.com"
};
객체 리터럴 방식으로 타입을 정의한다. 객체 구조를 명시적으로 지정하여 id, name, email 필드가 각각 number, stirng, string 타입임을 정의한다.
장점
단점
type User = {
id: number;
name: string;
email: string;
};
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com"
};
type 키워드를 사용하여 User라는 별칭을 만들어 객체의 타입을 정의한다. 타입을 재사용하거나 확장하는 데 적합하다.
장점
단점
extends)는 type 보다는 interface에 더 적합하다.interface User {
id: number;
name: string;
email: string;
}
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com"
};
interface를 사용하여 객체의 구조를 정의한다. 객체의 타입을 정의할 때 가장 일반적으로 사용되며, 클래스와도 호환된다.
장점
extends)단점
type User = {
id: number;
name: string;
email: string;
};
type UserRecord = {
[key: string]: User;
};
const users: UserRecord = {
admin: { id: 1, name: "Alice", email: "alice@example.com" },
user: { id: 2, name: "Bob", email: "bob@example.com" },
guest: { id: 3, name: "James", email: "james@example.com" }
};
동적인 데이터 타입을 정의할 때 주로 사용되며, 객체의 특정 속성값을 미리 알 수 없을 때 유용하다. (그 외의 경우에는 최대한 사용을 지양하는 것이 좋음)
위 예시에서는 문자열 키(key: string)와 User 타입의 값으로 구성된 객체를 정의했다.
장점
단점
type User = {
id: number;
name: string;
email: string;
};
type UserRecord = {
[key in 'admin' | 'user' | 'guest']: User;
};
// 기존 User의 속성들 각각에 readonly 제약 추가
type ReadOnlyUser = {
readonly [K in keyof User]: User[K];
};
// 각 속성을 옵셔널로 설정
type PartialUser = {
[K in keyof User]?: User[K];
};
기존 타입을 기반으로 새로운 타입을 생성할 때 사용된다. index signature와 달리, Mapped Type은 키의 집합을 제한할 수 있다.
위 예제에서는 'admin' | 'user' | 'guest' 를 키로, 각 키의 타입은 User 타입을 갖는 객체를 정의했다.
장점
readonly로 설정하거나 선택적(optional)로 변경하는 등의 작업을 간단히 수행할 수 있다.단점
// 예시 1
type User = {
id: number;
name: string;
email: string;
};
type UserRecord = Record<'admin' | 'user' | 'guest', User>;
const users: UserRecord = {
admin: { id: 1, name: "Alice", email: "alice@example.com" },
user: { id: 2, name: "Bob", email: "bob@example.com" },
guest: { id: 3, name: "James", email: "james@example.com" }
};
// 예시 2
// 객체의 키 타입 제한
type PermissionRecord = {
read: boolean;
write: boolean;
execute: boolean;
}
// 위 객체 타입을 Record를 이용해서 정리
type Permissions = 'read' | 'write' | 'execute';
type PermissionRecord = Record<Permissions, boolean>;
const userPermissions: PermissionRecord = {
read: true,
write: false,
execute: true
};
맵드 타입을 좀 더 간결한 형태로, 키와 값의 타입을 지정한다.
위 예제에서는 'admin' | 'user' | 'guest' 를 키로, 각 키의 값이 User 타입을 갖는 객체를 정의하고 있다.
장점
단점