타입스크립트 type guards로 타입 좁히기
Type Guard
타입 가드는 아래 설명을 참조하시면 좋습니다. (원문의 의미를 희석하지 않기 위해 본문을 일부 발췌하였습니다.)
Type Guards allow you to narrow down the type of an object within a conditional block. (출처 : https://basarat.gitbook.io/typescript/type-system/typeguard)
This way of reducing the size of a type is called narrowing. Checking the result of typeof and similar runtime operations are called type guards.(출처 : TypeScript: narrowing types via type guards and assertion functions)
즉 Type Guard를 통해 컴파일러가 타입을 예측할 수 있도록 타입을 좁혀 주어서(narrowing) 좀 더 type safety
함을 보장할 수 있습니다.
built-in Type Guard - typeof, instanceof
JavaScript에 이미 존재하는 typeof
, instanceof
등의 연산자를 활용해 Type Guard 할 수 있습니다.
typeof
연산자는 피연산자의 타입을 판단하여 문자열로 반환해 줍니다. 이 특성을 활용해서 타입스크립트에서는 아래와 같은 상황에서 사용할 수 있습니다.
1
2
3
4
5
6
7
8
function testFunc(arg: string | number) {
arg.substring(3); // ts(2339) : Property 'substring' does not exist on type 'string | number'.
if (typeof arg === "string") {
// 여기 밑에서 사용하는 arg는 반드시 string type으로 인식합니다.
arg.substring(3);
}
}
위처럼 함수 인자로 넘어온 arg
는 union type으로 string 혹은 number 일 수 있습니다.
이 상황에서 arg.substring 을 사용하면, 위처럼 substring
이 존재하지 않는다는 에러를 발생시킵니다. arg가 확실히 string인지 알 수 없기 때문입니다.
그러므로 그 밑에서 if (typeof arg === 'string')
을 사용해서, if 문 내에서는 arg가 무조건 string type임을 보장하게 합니다.
instanceof
연산자는, 판별할 객체가 특정한 클래스에 속하는지 확인할 수 있습니다.
(사실 자바스크립트에서 class는 prototype이라는 속성을 활용한 것이고, instanceof는 prototype 체인에 생성자의 prototype이 있는지 여부를 확인하는 방식으로 동작하는 것입니다. (자바스크립트 prototype))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Student {
name: string;
age: number;
}
class School {
location: string;
}
function testFunc(arg: Student | School) {
if (arg instanceof Student) {
console.log(arg.name); // OK
console.log(arg.location); // ts(2339) : Property 'location' does not exist on type 'Student'.
} else {
// 여기서부터는 arg가 반드시 School 타입으로 인식합니다.
console.log(arg.location); // OK
}
}
const student = new Student();
testFunc(student);
하지만 typeof
피연산자로 올 수 있는 것은 primitives types 및 객체로 제한이 됩니다. (참고로 null type은 typeof 시 ‘object’를 반환합니다.typeof-MDN Web Docs)
즉, 타입스크립트는 사용자가 직접 type 혹은 interface 를 사용해 타입을 지정할 수 있는 장점도 매우 큰데, 이러한 것들은 typeof
로 검사를 할 수 없습니다.
따라서 아래와 같이 user defined type guards
를 작성하면 해결할 수 있습니다.
User Defined Type Guards
아래와 같이 Animal
, Flower
interface를 정의했다고 가정해 보겠습니다.
Animal
에만 name 프로퍼티가 존재하고, Flower
에만 type 프로퍼티가 존재합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
interface Animal {
name: string;
age: number;
}
interface Flower {
type: string;
age: number;
}
interface ExampleInfo {
page: number;
infoBody: Animal | Flower;
}
// User Defined Type Guards Function
// 인자로 넘어온 값에 name 프로퍼티가 있다면 arg의 타입을 Animal로 인식하게 한다.
function isAnimal(arg: any): arg is Animal {
return arg.name !== undefined;
// return 'name' in arg; 라고 표현해도 동일합니다.
}
function doSomething(arg: ExampleInfo) {
const { page, infoBody } = arg;
if (isAnimal(infoBody)) {
// 이 if문 안에서 infoBody의 타입은 반드시 Animal 인터페이스이다.
console.log(infoBody.name);
} else {
// 이 if문 안에서 infoBody의 타입은 반드시 Flower 인터페이스이다.
console.log(infoBody.type); // OK
console.log(infoBody.name); // error
}
}
const puppy: Animal = {
name: "puppy",
age: 5,
};
const animalInfo: ExampleInfo = {
page: 10,
infoBody: puppy,
};
doSomething(animalInfo);
보통 타입스크립트로 코드를 작성할 때, 위와 같은 interface 를 정의하는 경우가 많습니다.
위의 경우 특정 프로퍼티의 유무
에 따라 type guards를 한 상황입니다.
즉, isAnimal
함수는 인자로 받은 값에 name 프로퍼티가 있으면 타입을 Animal
인터페이스로 평가합니다. 따라서 그 안에서 인자의 type이 Animal
임이 보장되므로 안심하고 name 프로퍼티를 사용할 수 있습니다.
위에서 user defined type guards 함수의 판단조건은 특정 프로퍼티의 유무
였는데요, 여기에서 리턴하는 부분을 조금만 수정하면, 판단 조건을 원하는 대로 바꿀 수 있습니다.
예를 들어, 특정 객체의 프로퍼티의 값에는 숫자가 오는데, 특정 숫자일 때와 아닐 때에 따라 타입을 다르게 구분하고 싶다면 아래와 같이 작성해볼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface ZeroBody {
age: 0; // 반드시 0만 가능하다는 의미
name: string;
}
interface OtherBody {
age: number;
name: string;
}
interface Response {
type: string;
body: ZeroBody | OtherBody;
}
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);
}
}
body.age 값이 0이면 body에 오는 값의 타입은 ZeroBody이고, 0이 아니면 타입이 OtherBody로 보장할 수 있습니다.
이처럼 조건별로 다른 타입으로 좁히고 싶을 때 원하는 대로 user defined type guards 함수를 작성하여 활용할 수 있습니다.
레퍼런스