타입 가드(Type Guard)
들어가며
- 타입스크립트(TypeScript)의 타입 가드(Type Guard)에 대해 정리해본다.
타입 가드(Type Guard)
개념
- 타입스크립트에서 변수의 타입을 좁히는 방법
- 주로 조건문(Conditional Statement)을 사용하여 특정 타입임을 확인한 후, 해당 타입에 맞는 안전한 작업을 수행할 수 있도록 도와준다.
- 타입 가드를 사용하면 컴파일 시점에 타입 오류를 방지하고, 코드의 가독성과 안정성을 높일 수 있다.
- 타입 가드를 효과적으로 사용하면 런타임 오류를 줄이고, 개발 과정에서 타입 관련 버그를 미리 방지할 수 있으며, 코드의 의도를 명확하게 표현하여 협업 시 가독성을 높이는 데 큰 도움이 된다.
타입 가드는 런타임에서 변수의 타입을 확인하여 타입스크립트에게 해당 변수의 타입을 좁히도록 지시하는 역할을 한다.
방법
① typeof 연산자 이용하기
- @typeof@ 연산자를 이용하는 방법이다.
- @typeof@ 연산자는 변수의 기본 타입을 확인할 때 사용된다.
- 주로 @string@, @number@, @boolean@과 같은 원시 타입(Primitive Type)을 구분할 때 유용하다.
예제 코드
type ValueType = string | number | boolean;
let value: ValueType;
const random = Math.random();
value = random < 0.33 ? 'Hello' : random < 0.66 ? 123.456 : true;
function checkValue(value: ValueType) {
if (typeof value === 'string') {
console.log(value.toLowerCase());
return;
}
if (typeof value === 'number') {
console.log(value.toFixed(2));
return;
}
console.log(`boolean: ${value}`);
}
checkValue(value);
⇒ @checkValue@ 함수는 @ValueType@ 타입의 매개변수를 받는다.
⇒ @typeof@를 사용하여 @value@의 타입을 검사하고, 각 타입에 맞는 처리를 수행한다.
② 동등성 좁히기(Equality Narrowing)
- 동등성 검사(@===@, @!==@)를 사용하여 변수의 타입을 좁히는 방법
- 주로 유니온(Union) 타입에서 특정 리터럴 값으로 타입을 구분할 때 유용하다.
예제 코드
type Dog = { type: 'dog'; name: string; bark: () => void };
type Cat = { type: 'cat'; name: string; meow: () => void };
type Animal = Dog | Cat;
function makeSound(animal: Animal) {
if (animal.type === 'dog') {
animal.bark();
} else {
animal.meow();
}
}
⇒ @Animal@ 타입은 @Dog@ 또는 @Cat@일 수 있다.
⇒ @animal.type@을 비교하여 @Dog@인지 @Cat@인지 구분하고, 각각의 메서드를 호출한다.
③ in 연산자 이용하기
- @in@ 연산자를 이용하는 방법이다.
- @in@ 연산자를 이용하여 객체에 특정 속성이 있는지 확인함으로써 타입을 좁힐 수 있다.
예제 코드
type Dog = { type: 'dog'; name: string; bark: () => void };
type Cat = { type: 'cat'; name: string; meow: () => void };
type Animal = Dog | Cat;
function makeSound(animal: Animal) {
if ('bark' in animal) {
animal.bark();
} else {
animal.meow();
}
}
⇒ @bark@ 메서드가 @animal@ 객체에 있는지 확인하여 @Dog@인지 @Cat@인지 구분한다.
④ Truthy/Falsy 값 이용하기
- Truthy/Falsy 값을 이용하여 변수의 타입을 좁히는 방법이다.
- 주로 @null@, @undefined@, 빈 문자열 등을 확인할 때 사용된다.
(참고) Truthy한 값과 Falsy한 값
Truthy한 값 | Falsy한 값 |
@true@, @1@, @'hello'@(빈 문자열이 아닌 모든 문자열), @[]@(빈 배열), @{}@(빈 객체), @Infinity@(양/음의 무한대) | @false@, @0@, @""@, @''@, @null@, @undefined@, @NaN@(Not-a-Number) |
예제 코드
function printLength(str: string | null | undefined) {
if (str) {
console.log(str.length);
} else {
console.log('No string provided');
}
}
printLength('Hello'); // 5
printLength(null); // No string provided
printLength(undefined); // No string provided
⇒ @str@이 Truthy 값인 경우 문자열의 길이를 출력하고, 그렇지 않으면 '@No string provided'@를 출력한다.
⑤ instanceof 연산자 이용하기
- @instanceof@ 연산자를 사용하여 객체가 특정 클래스의 인스턴스인지 확인한다.
- 주로 클래스 기반 객체를 구분할 때 유용하다.
예제 코드
function checkInput(input: Date | string): string {
if (input instanceof Date) {
return input.getFullYear().toString();
}
return input;
}
const year = checkInput(new Date());
const random = checkInput('2020-05-05');
console.log(year); // 현재 연도
console.log(random); // '2020-05-05'
⇒ @input@이 @Date@ 객체인지 확인하고, 맞다면 연도를 문자열로 반환한다.
⇒ 그렇지 않으면 원래 문자열을 반환한다.
⑥ 타입 프리디케이트(Type Predicate) 이용하기
- 타입 프레디케이트(Type Predicate)는 함수의 반환(return) 타입이 특정 타입임을 명시하여 타입을 좁히는 방법이다.
- 주로 사용자 정의 타입 가드를 만들 때 사용된다.
- @is@ 연산자를 이용한다.
- @is@ 연산자는 해당 변수가 특정 타입인지 확인해준다.
예제 코드
type Student = {
name: string;
study: () => void;
};
type User = {
name: string;
login: () => void;
};
type Person = Student | User;
// 타입의 반환 타입이 특정 타입임을 명시하기 (person is Student)
function isStudent(person: Person): person is Student {
return (person as Student).study !== undefined;
}
const person: Person = {
name: 'anna',
study: () => console.log('Studying'),
};
if (isStudent(person)) {
person.study(); // Student 타입으로 좁혀짐
} else {
person.login();
}
⇒ @isStudent@ 함수는 @Person@ 타입의 매개변수를 받아 @Student@ 타입인지 확인한다.
⇒ 타입 프레디케이트(@person is Student@)를 사용하여 타입을 좁힌다.
⇒ @if@ 블록에서는 @person@이 @Student@임을 타입스크립트가 인식하여 @study@ 메서드를 안전하게 호출할 수 있다.
⑦ 사용자 정의 타입 가드 이용하기
- 개발자가 직접 타입 가드 함수를 정의할 수 있다.
- 사용자 정의 타입 가드를 통해 복잡한 타입이나 커스텀 타입에 대한 타입 가드를 구현할 수 있다.
예제 코드
interface Vehicle {
make: string;
}
interface Car extends Vehicle {
drive: () => void;
}
interface Truck extends Vehicle {
haul: () => void;
}
type VehicleType = Car | Truck;
function isCar(vehicle: VehicleType): vehicle is Car {
return (vehicle as Car).drive !== undefined;
}
const myVehicle: VehicleType = {
make: 'Toyota',
drive: () => console.log('Driving'),
};
if (isCar(myVehicle)) {
myVehicle.drive();
} else {
myVehicle.haul();
}
⇒ @isCar@ 함수는 @VehicleType@이 @Car@인지 확인하는 사용자 정의 타입 가드이다.
⇒ 이를 통해 @Car@와 @Truck@을 안전하게 구분하여 각 타입에 맞는 메서드를 호출할 수 있다.
⑧ Discriminated Unions와 never 타입 이용하기
- Discriminated Unions는 유니온 타입의 각 멤버가 공통된 리터럴 속성을 가지는 패턴을 의미하며, 이를 통해 타입을 쉽게 구분할 수 있다.
- 또한, @never@ 타입을 이용하여 모든 경우를 처리했는지 컴파일 타임에 검증할 수 있다.
예제 코드
type IncrementAction = {
type: 'increment';
amount: number;
timestamp: number;
user: string;
};
type DecrementAction = {
type: 'decrement';
amount: number;
timestamp: number;
user: string;
};
type Action = IncrementAction | DecrementAction;
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'increment':
return state + action.amount;
case 'decrement':
return state - action.amount;
default:
const exhaustiveCheck: never = action;
throw new Error(`Unexpected action: ${exhaustiveCheck}`);
}
}
const newState = reducer(15, {
user: 'john',
type: 'increment',
amount: 5,
timestamp: 123456,
});
⇒ @Action@ 타입은 @IncrementAction@ 또는 @DecrementAction@일 수 있다.
⇒ @reducer@ 함수는 @action.type@을 기준으로 분기하여 각 타입에 맞는 로직을 수행한다.
⇒ @default@ 케이스에서 @never@ 타입을 사용하여 모든 가능한 액션 타입이 처리되었는지 컴파일 시점에 확인한다.
타입 카드 활용 예제
예제 1 : 복합 데이터 타입 처리
type ApiResponse = SuccessResponse | ErrorResponse;
interface SuccessResponse {
status: 'success';
data: any;
}
interface ErrorResponse {
status: 'error';
error: string;
}
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
console.log('Data:', response.data);
} else {
console.error('Error:', response.error);
}
}
⇒ @ApiResponse@는 성공 응답과 오류 응답을 나타낼 수 있다.
⇒ @status@ 속성을 기준으로 타입을 구분하고, 각 타입에 맞는 처리를 수행한다.
예제 2 : 클래스 인스턴스 구분
class Admin {
name: string;
privileges: string[];
constructor(name: string, privileges: string[]) {
this.name = name;
this.privileges = privileges;
}
}
class Employee {
name: string;
startDate: Date;
constructor(name: string, startDate: Date) {
this.name = name;
this.startDate = startDate;
}
}
type Person = Admin | Employee;
function printPersonInfo(person: Person) {
console.log(`Name: ${person.name}`);
if (person instanceof Admin) {
console.log('Privileges:', person.privileges);
} else {
console.log('Start Date:', person.startDate);
}
}
const admin = new Admin('Alice', ['manage-users']);
const employee = new Employee('Bob', new Date());
printPersonInfo(admin);
printPersonInfo(employee);
⇒ @Person@ 타입은 @Admin@ 또는 @Employee@일 수 있다.
⇒ @instanceof@를 사용하여 클래스 인스턴스를 구분하고, 각 클래스에 맞는 정보를 출력한다.
사용 시 주의사항
- 타입 가드는 컴파일 시점에 타입을 좁히는 것이 아니라, 런타임에서 실제 값의 타입을 확인한다.
- 따라서 타입 가드 조건이 정확해야 한다.
- 타입 가드를 사용한 후에도 변수의 타입이 일관되게 유지되도록 코드를 작성해야 한다.
- 예를 들어, 타입 가드 후에도 변수의 속성이 존재하지 않는 경우 오류가 발생할 수 있다.
- 모든 가능한 타입을 처리했는지 확인하기 위해 @never@ 타입을 활용할 수 있다.
- 특히 유니온 타입을 사용할 때 유용하다.
- 복잡한 객체나 다중 조건이 필요한 경우 사용자 정의 타입 가드를 정의하여 코드의 가독성과 재사용성을 높일 수 있다.
참고 사이트
'Programming > TypeScript' 카테고리의 다른 글
[TypeScript] 클래스(Class) (0) | 2024.10.12 |
---|---|
[TypeScript] 제네릭(Generic) (0) | 2024.10.12 |
[TypeScript] 인터페이스(Interface) (0) | 2024.10.12 |
[TypeScript] Zod 라이브러리 (0) | 2024.10.11 |
[TypeScript] 모듈 방식 사용하기 (0) | 2024.10.10 |
[TypeScript] 인터페이스(Interface)와 타입 별칭(Type Alias) 비교 (0) | 2024.10.09 |
[TypeScript] ! 연산자(Non-null Assertion Operator) (0) | 2024.08.20 |
[TypeScript] MODULE_NOT_FOUND (Error: Cannot find module ~\react-scripts\bin\react-scripts.js) 오류 해결 방법 (0) | 2024.08.20 |