[Typescript] 타입 단언 / 타입 가드 / 타입 호환
요즘 타입스크립트 강의를 들으며 개념을 상기하고 연습예제를 풀 준비를 하고 있다.
기본적인 내용들은 이전에 정리한 포스팅을 복기하였으나, 블로그에 남기지 않은 내용 중 중요한 부분을 이어서 포스팅하려고 한다.
이번 글은 타입스크립트 입문 강의를 들으면서, 타입을 다루는 주요 개념들을 정리해보려고 한다.
- 타입을 명시하는 타입 단언(Assertion)
- 유니온 등에서 타입의 경우를 좁혀가는 타입 가드(Guard)
- 타입 간의 호환성을 판단하는 타입 호환(Compatibility)
* 이 포스팅은, 인프런 캡틴판교 님의 '타입스크립트 입문' 강의를 듣고 정리한 내용을 바탕으로 작성하는 시리즈입니다. (링크)
💙 Type Assertion (타입 단언)
타입 단언이란, 타입스크립트가 추론하지 못하는 타입을 개발자가 직접 명시해주는 문법이다.
컴파일러가 실제 런타임에 존재하는 변수 타입과 다르게 추론하거나, 너무 보수적으로 추론하는 경우 개발자가 수동적으로 조작하기 위해 많이 사용된다.
사용법은 아래와 같다.
[변수] as [타입]
이는, 타입 선언 혹은 타입을 변환하는 Type Casting과는 달리, 실제 데이터 타입을 변경하지 않고 에러만 방지하는 기능이다.
- 사용예시
타입 단언은 기본적인 as 키워드를 활용한 문법, 그리고 <타입>을 변수에 부여하는 문법 2가지로 구현 가능하다.
// as
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
// 꺾쇠(Angle bracket), 타입 단언
let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length; // unknown 타입이지만 Type Assertion을 통해 타입추론 가능
* 단, 두 번째 경우는 HTML 태그 등과 혼동되므로 as 키워드를 많이 사용하는 것이다.
- 사용예제 : DOM API 조작
나도 방금 과제를 하면서 느꼈지만, 이 타입 단언은 DOM 조작에 유용하게 쓰일 수 있다.
먼저 아래 예시를 보자. 변수(div)에 DOM을 할당하고 내부의 프로퍼티를 사용하려고 한다.
const div = document.querySelector('div');
console.log(div.innerText); // error!
if (div) console.log(div.innerText)
에러가 발생하는 이유는, 기본적으로 DOM은 HTMLDivElement | null 의 유니온 타입으로 이루어져 있기 때문이다.
그렇기에, 해당 요소가 존재하지 않으면 null이 할당되므로 특정 프로퍼티 접근에 대해 경고를 내리는 것이다.
이 경우, 개발자는 확실히 div가 HTML 요소임을 단언하기 위해 해당 문법이 사용되는 것이다.
const div = document.querySelector('div') as HTMLDivElement;
console.log(div.innerText); // no error
- 타입 선언(Declaration)과 단언(Assertion)의 차이
type Color = {
red: () => void;
yellow: () => void;
}
// Type Declaration
const colorDec = {
red() {
console.log('빨강!');
},
yellow() {
console.log('노랑!');
},
}
// Type Assertion
const colorAss = {} as Color
- 타입 선언(Declaration) : 객체 프로퍼티들을 모두 강제하기 때문에 휴먼 에러를 방지할 수 있다.
- 타입 단언(Assertion) : 개발자의 단언에 따라 Color 타입으로 신뢰하여 별도의 에러가 발생하지 않는다.
💙 Type Guard (타입 가드)
유니온 타입을 사용하면서, 복수의 타입들 중 어느 타입을 이용할 지 확신을 주는 문법이다. (그래서, Type Narrowing 이라고도 한다.)
먼저, 관련 예시를 살펴보자.
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
function introduce(): Developer | Person {
return { name : 'Tony', age : 33, skill: 'Iron Marking'}
}
let tony = introduce();
console.log(tony.skill); // error
위 예시처럼, tony라는 인스턴스를 만들었다고 가정하자.
이는, union 타입이기에 name, age, skill 모든 프로퍼티 설정이 가능하나, 인스턴스에서 skill에 접근하려하는 경우 에러가 발생한다.
유니온 타입의 인스턴스는 기본적으로 공통된 프로퍼티만 허용하기에, name 외의 프로퍼티에서 에러가 발생하는 것이다.
* 타입 단언(Assertion) 으로 해결하는 경우
if ((tony as Developer).skill) {
console.log(tony.skill);
}
else if ((tony as Person).age) {
console.log(tony.age);
}
이처럼 tony(인스턴스)의 타입을 단언으로 보장한 뒤 조건문으로 설정하는 방법도 있지만, 보시다시피 가독성과 효율이 매우 떨어진다.
- 사용방법 : 사용자 정의 Type Guard
// 타입가드 정의
function isDeveloper(target: Developer | Person): target is Developer {
return (target as Developer).skill !== undefined;
}
// 타입가드 활용
if (isDeveloper(tony)) {
console.log(tony.skill) // Developer 타입으로 추론됨
}
else {
console.log(tony.age) // Person 타입으로 추론됨
}
먼저, isDeveloper() 라는 Type Guard 함수를 정의한다. 인자의타입이 Developer인지 여부를 반환한다. (as 단언문은 생략가능)
이 타입 가드 함수는 Boolean을 반환하기 때문에 예시처럼 활용하면 된다.
이 때 중요한 점은, if와 else 각 scope에서 인자(tony)의 타입을 Developer인 경우와 아닌 경우(여기서는 Person만)로 추론해준다는 것이다.
- 타입가드 문법
위는 직접 정의한 타입가드 함수이고, 타입스크립트는 기본적으로 타입 가드를 위한 문법들을 몇 가지 지원하고 있다.
1) typeof
function isString(value: string | number) {
if (typeof value === string) console.log(value.substr(0));
else console.log(Math.floor(value));
}
typeof 연산자를 통해 인자(value)의 유니온 타입을 확인한다.
2) instanceOf
class Developer {
developer() {
console.log("업무 중")
}
}
class Designer {
design() {
console.log("업무 중")
}
}
const work = (worker: Developer | Designer) => {
if (worker instanceof Developer) {
worker.developer();
} else {
worker.design();
}
}
타입스크립트는 클래스도 일종의 타입이다.
인스턴스를 typeof로 판정하면 항상 객체(object)이므로, instanceOf 문법을 통해 어느 클래스에서 비롯됬는지를 판정하는 것이다.
3) in
type Human = {
think: () => void;
};
type Dog = {
tail: string;
bark: () => void;
}
declare function getEliceType(): Human | Dog;
const elice = getEliceType();
if ('tail' in elice) {
elice.bark();
} else {
elice.think();
}
[문자열] in [객체] 문법은 객체 내에 해당 프로퍼티가 존재하는지를 반환한다. 객체 프로퍼티에 접근하는 경우가 많기에 유용한 문법이다.
💙 Type Compatibility (타입 호환)
타입 호환은, 타입스크립트에서 특정 타입이 다른 타입에 잘 맞는지를 의미한다. 코드 타입을 해석해나가는 과정에서 두 타입이 서로 호환되는지를 점검하는 것이다.
이는, 객체 변수에 인터페이스를 적용할 때 이해해야 할 주요 개념이다.
- 구조적 타이핑
구조적 타이핑(structural typing)은 코드 구조 관점에서 타입이 서로 호환되는지 여부를 판단하는 것이다.
interface Restaurant {
name: string;
star: number;
}
let pinetree: Restaurant; // pinetree 변수 선언
let pinetreeWithAddress = { // pinetreeWithAddress 변수 선언하고 초기화
name: "대나무한정식",
star: 5,
address: "동대문",
};
pinetree = pinetreeWithAddress;
pinetree의 Restaurant 타입은 2개 프로퍼티(name, star) 를,
pinetreeWithAddress의 추론된 타입은 3가지 프로퍼티(name, star, address) 를 각각 가지게 된다.
그래서, pinetree에 pinetreeWithAddress 변수를 할당하면 에러가 날 것으로 예상했지만, 서로가 잘 호환되고 있다.
타입스크립트는 할당하는 타입이 할당받는 타입의 구조보다 크다면 서로 호환된다고 인정하는 것이다.
이를, 구조적 타이핑(structual typing)이라 한다.
- 예제 : 인터페이스, 클래스
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
}
class PersonClass {
name: string;
}
let developer: Developer;
let person: Person;
person = developer;
developer = person; // Error!
developer = new PersonClass() // Error!
person과 developer 변수의 각 타입을 인터페이스로 작성한 뒤, 이를 서로에게 각각 할당하는 예제이다.
developer는 name 프로퍼티를 보장하기에 person에 할당되어도 무방하나,
person은 skill 프로퍼티를 보장하지 않기에 developer에 할당될 수 없는 것이다.
이는, 클래스(PersonClass)도 동일하다. 클래스도 일종의 타입으로, 인스턴스의 타입이 호환되지 않는 경우 에러가 발생하는 것이다.
- 예제 : 함수
const add = function(a: number) {
return a;
}
const sum = function(a: number, b: number) {
return a + b;
}
sum = add;
add = sum // Error!
함수에서도 마찬가지이다. sum은 2개의 변수(number)를, add는 1개 변수(number)를 받는다.
그렇기에 sum이 더 큰 범위라고 간주하여, 아래의 할당문이 에러가 발생한다.
함수의 매개변수, 혹은 반환값의 타입의 범주가 더 큰 값이 할당되어야 타입이 호환되는 것으로 판단하는 것이다.
- 예제 : 제네릭
interface Empty<T> {
// ..
}
let empty1: Empty<string>
let empty2: Empty<number>;
empty1 = empty2
empty2 = empty1;
interface NotEmpty<T> {
data: T;
}
let notempty1: NotEmpty<string>;
let notempty2: NotEmpty<number>;
notempty1 = notempty2 // Error!
notempty2 = notempty1 // Error!
제네릭으로 타입 인자를 받는 인터페이스를 예시로 들었다. 위 2가지 케이스를 비교해보자.
Empty의 경우, 제네릭으로 할당되는 타입에 따라 내부 프로퍼티가 영향을 받지 않기 때문에 에러가 발생하지 않는다.
하지만 NotEmpty의 경우, 제네릭에 따라 data 타입이 바뀌기 때문에 notempty1, notempty2 는 서로 호환되지 않는 것이다.
이직을 마치고, 입문강의를 오랜만에 다시 이어 듣게 되면서 포스팅이 늦어졌다..!!
타입스크립트를 처음 공부하면서 기본적인 타입개념과 문법은 정리했지만, 이를 좀 더 능숙하게 다루는 단언이나 가드는 처음 정리하게 되었다.
그렇기에 각 주제들이 연관성이 있다기 보단, 강의 배치상 붙어있는 개념들을 한 데 모아 포스팅하게 된 것이다.
📎 참조
- [Typescript] 공식문서 : https://www.typescriptlang.org/docs/
- [TS/강의] 인프런 강의(캡틴판교) : https://www.inflearn.com/course/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9E%85%EB%AC%B8
- [TS/강의] 타입스크립트 핸드북(캡틴판교 저) : https://joshua1988.github.io/ts/
- [전체] steadily-worked 님의 블로그 : https://steadily-worked.tistory.com/536
- [단언, 가드] yceffort 님의 블로그 : https://yceffort.kr/2019/08/20/typescript-type-assertion
- [가드] kyoung-jnn 님의 블로그 : https://lakelouise.tistory.com/191#%F0%9F%93%9D-literal-type-guard
- [호환] bohyunkang 님의 블로그 : https://bohyunkang.tistory.com/40