union type의 문제점은 바로 공통된 프로퍼티만 사용을 할 수 있다는 것이었습니다
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
// Developer | Person 타입을 리턴
function introduceSelf(): Developer | Person {
return {
name: "minho",
age: 28,
};
}
// Developer | Person 이므로 공통 특성인 name만 사용가능
const minho = introduceSelf();
console.log(minho.age); // error
이러한 문제점을 막기 위해 사용하는 것이 바로 type guard입니다
type guard를 통해 컴파일러가 타입을 예측할 수 있도록 타입을 좁혀 주어서(narrowing) 좀 더 타입의 안전성을 보장할 수 있습니다.
JS에 이미 존재하는 typeof, instanceof 등의 연산자
를 활용해서
type guard를 할 수 있습니다
typeof 연산자는 피연산자의 타입을 판단하여 문자열로 반환해 줍니다.
이 특성을 활용해서 아래의 예제는
if 문 내에서 arg는 무조건 string type임을 보장
하게 합니다.
const testFunc = (arg: string | number) => {
// arg.substring(3); // error
if (typeof arg === 'string') {
// 여기 밑에서 사용하는 arg는 string type으로 인식합니다.
arg.substring(3);
}
};
그렇지만 typeof의 치명적인 단점 2가지가 있습니다
typeof 피연산자로 올 수 있는 것은 primitive 타입 및 객체
로 제한이 됩니다.
즉, 타입스크립트는 사용자가 직접 지정한 타입
혹은 인터페이스
를 사용해 타입을 지정할 수 있는 장점도 매우 큰데,
이러한 것들을 typeof로 검사를 할 수 없는 것입니다.
class Developer {
skill: string = 'a';
}
class Person {
age: number = 32;
}
function introduceSelf(arg: Developer | Person) {
if (typeof arg === 'Developer') { // error
// 직접 지정한 타입은 typeof로 검사 불가
console.log(arg.skill);
}
}
null type은 typeof 시 ‘object’로 반환
해버린다는 치명적인 단점이 있습니다
instanceof
연산자는, 판별할 객체가 특정한 클래스에 속하는지 확인할 수 있습니다.
class Student {
name: string = 'MINO';
age: number = 28;
}
class School {
location: string = 'seoul';
}
function testFunc(arg: Student | School) {
if (arg instanceof Student) {
// arg가 반드시 Student 타입으로 인식
console.log(arg.name);
} else {
// arg가 반드시 School 타입으로 인식
console.log(arg.location); // OK
}
}
const student = new Student();
testFunc(student);
instanceof 또한 단점이 있습니다
instanceof는 인터페이스에 사용이 불가능합니다.
말 그대로 인스턴스이기 때문에 실제 값으로도 사용이 가능해야 합니다.
그러므로 오로지 클래스에만 사용이 가능합니다
interface Developer {
skill: string;
}
interface Person {
age: number;
}
function introduceSelf(arg: Developer | Person) {
if (arg instanceof Developer) { **// error**
// 'Developer' only refers to a type, but is being used as a value here.
console.log(arg.skill);
}
}
그러므로 typeof 나 instanceof 말고 user defined type guards 를 작성하는 것이 좋습니다
is 키워드를 사용해서 특정 프로퍼티의 유무에 따라 type guards를 할 수 있습니다
type guards를 하는 함수는 항상 리턴값으로 우리가 비교하자고자 하는 대상의 프로퍼티에 대해서 true , false값을 리턴합니다
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
function introduceSelf(): Developer | Person {
// 인터페이스 타입 Developer | Person 중에서 Person 형태 사용
return {
name: 'minho',
age: 23,
};
}
// 현재 minho는 Developer | Person 타입이라 name프로퍼티만
// 사용이 가능
const minho = introduceSelf();
// User Defined Type Guards Function
// 인자로 넘어온 값에 skill 프로퍼티가 있다면 arg의 타입을 Developer로 인식
function isDeveloper(target: Developer | Person): target is Developer {
return (target as Developer).skill !== undefined;
// return 'skill' in target; 로 사용해도 동일함
}
// type guard를 한 번 거친 후에는 타입에 따른 프로퍼티를 사용 가능
if (isDeveloper(minho)) {
console.log(minho.skill);
} else {
console.log(minho.age);
}
💡 아무 조건없이 union type을 사용했을때 공통 프로퍼티만 접근이 가능한 것일 뿐이지 지금은 type guard로 구분하고 있으므로 무조건 name이라는 공통 프로퍼티를 문법적으로 작성할 필요는 없습니다
여기에 리턴 타입으로 is로 type guards를 해준 것이므로
skill프로퍼티가 있으면 타입을 Developer인터페이스로 평가하게 되고
이제 이 type guard를 한 번 거치고 난 다음에는 타입에 따른 프로퍼티를 사용할 수 있는 것입니다
if (isDeveloper(minho)) {
console.log(minho.skill);
// 원래는 minho는 여전히 union Type이기 때문에
// (조건문에 상관없이) minho.skill 형태 자체가 불가능했지만
// type guards를 거쳤으므로 사용이 가능합니다
}
else {
console.log(minho.age);
}
예를 들어, 특정 객체의 프로퍼티의 값에는 숫자가 오는데,
특정 숫자일 때 or 아닐 때에 따라 타입을 다르게 구분하고 싶다면 아래와 같이 작성해볼 수 있습니다.
interface ZeroBody {
age: 0; // 반드시 0만 가능하다는 의미
name: string;
}
interface OtherBody {
age: number;
name: string;
}
interface Response {
type: string;
body: ZeroBody | OtherBody;
}
// type guard를 통해 age의 값이 0인 경우에만
// ZeroBody으로 사용되게 함
function isZero(arg: any): arg is ZeroBody {
return arg.age === 0;
}
function doSomething(arg: Response) {
const { type, body } = arg;
if (isZero(body)) {
console.log(body.age); // 0
} else {
// 여기의 body는 OtherBody
console.log(body.age);
}
}