📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄

[TypeScript] 타입 단언으로 변수에 타입 부여해주기 (vs 타입 선언)

타입 선언과 타입 단언

변수에 타입을 부여하는 방식에는 가장 일반적인 방법인 타입 선언이 있다. 또한 오늘 설명할 타입 단언이라는 것도 있다.

내용을 미리 스포(?)하자면, 타입스크립트 책에서도 웬만하면 타입 선언으로 타입을 부여하고 타입 단언은 특별한 상황이 아니면 사용하지 말라고 되어있다.

 

타입 단언은 어떻게 쓰는 것이고 무슨 허점이 있길래 되도록이면 사용을 하지 말라고 하는지 한번 살펴보자.

 

타입 단언으로 타입 부여하기

type Person = {
    name: string;
    color: string;
}

// 오류 뜨는 경우
let user: Person = {}; // 지금 말고 추후에 추가하고 싶은데.. name과 color가 없다고 오류가 뜨네?
// 정상 작동하는 경우
let user: Person = {} as Person; // user 변수를 Person 타입으로 단언
user.name = 'shawn';
user.color = 'red';

타입 단언은 타입 선언의 리버스라고 보면 된다. 타입 선언의 경우 처음부터 타입을 정하고 그 타입에 맞는 값을 넣는 방식이라면 타입 단언은 일단 변수에 값을 할당하고 변수 뒤에 as를 통해 타입을 부여해 주는 방식이다.

 

위의 코드를 보면 Person이라는 타입 객체가 있고, user라는 객체가 있다. user의 타입은 Person이기 때문에 name과 color 프로퍼티가 필수로 들어가야 한다. 만약, 프로퍼티 값을 지금이 아닌 추후에 넣을 예정이라서 빈 객체 상태로 두고 싶다면 어떻게 해야 할까? 이럴 때는 타입 단언으로 "지금은 빈 객체지만, 이 객체는 결국 Person 타입으로 만들 거야!"라고 단언해줘야 한다. 그럼, Person이라는 타입을 유지한 채로 빈 객체 유지가 가능하다.

 

타입 단언의 조건

// A as B : A와 B는 서브 타입 혹은 슈퍼 타입 관계여야 한다.
let num1 = 1 as number;
let num2 = 1 as unknown;
let num3 = 1 as never;
let num4 = 1 as string; // Error!

타입 선언이 앞에서 타입을 부여해 주는 거라면, 타입 단언은 뒤에서 타입을 부여해 준다.

부여하는 방법으로는 '값 as 타입' 형식으로 값과 타입의 관계는 서브 타입 혹은 슈퍼 타입 관계여야 한다는 조건이 있다.

 

위 예시 코드를 보면 값의 자리에 1이라는 숫자 타입이 들어와 있다. 그럼 as 뒤의 타입 자리에는 숫자 타입과 동일한 단계의 타입이 오면 안 되고, 슈퍼 타입인 unknown 혹은 서브 타입인 never 등이 와야 한다. (물론 number 타입이 와도 된다.)

이게 타입 단언의 조건이다. 반드시 단언하려는 타입은 값의 타입이거나 슈퍼 타입, 서브 타입이어야만 한다.

 

다중 단언

let num = 1 as unknown as string; // 값은 number 타입이지만 변수 타입은 string

그럴리는 없겠지만, 만약 굳이 같은 단계의 타입으로 단언해주고 싶다면 위 코드처럼 다중 단언을 사용하면 된다.

먼저 슈퍼 타입인 unknown 타입으로 단언해 준 뒤, unknown 타입의 서브 타입인 string 타입으로 단언해 준다.

그럼 값은 숫자 타입이지만, num의 타입은 string 타입이라고 되어 있는 것을 볼 수 있다. 그런데 굳이...?

 

const 단언

let num = 1 as const; // 리터럴 타입

let user = {
    name: 'shawn',
    color: 'red'
} as const; // 모든 프로퍼티 readonly

리터럴 타입으로 단언해 주는 방법도 있다. 사실 이것도 그냥 const로 선언해 주면 되지만, 굳이 타입 단언으로 하겠다면 as의 타입 자리에 const를 써주면 된다. 객체의 경우에는 모든 프로퍼티에 readonly 속성을 주고 싶을 때 사용하면 유용할 것 같다.

 

Non Null 단언 (옵셔널 체이닝이 걸려있는 프로퍼티의 경우)

type User = {
    name: string;
    color?: string; // 옵셔널 체이닝 (있을 수도 있고 없을 수도 있고..)
}

let user: User = {
    name: 'shawn',
    color: 'red'
}

let userColor = user.color.length; // Error! (user.color는 undefined 일 수 있다.)
let userColor = user.color!.length; // user.color는 무조건 있다.

위 코드에서 타입 객체 User의 color 프로퍼티에는 '?'가 달려있다. 이 물음표의 정체는 옵셔널 체이닝으로 값이 있으면 값을 반환하고 값이 없으면 undefined를 반환하는 문법이다. color 프로퍼티에 '?'가 달려있기 때문에 User 타입을 사용하는 객체는 color 프로퍼티가 없어도 오류가 발생하지 않는다. 대신, 타입 객체에 옵셔널 체이닝이 있는 경우 한 가지 주의할 점이 있다.

 

옵셔널 체이닝이 걸려 있는 프로퍼티를 사용하는 경우에는 타입스크립트가 헷갈려한다. 왜냐하면 이 값은 진짜 있을 수도 있지만 없을 수도 있기 때문이다. 따라서, undefined 타입을 포함한 union 타입으로 반환된다. 위 코드에서 user.color는 string과 undefined의 union 타입으로 정의되었다. 

 

user.color가 undefined 타입일 수도 있다는 가능성 때문에 user.color의 length 속성을 가져오는 데 오류가 발생한다.

따라서, 우리는 옵셔널 체이닝이 걸려있는 프로퍼티를 향해 "우리는 undefined 타입이 아니야!"라는 것을 어필해줘야 한다. 그 방법이 바로 '?'를 '!'로 바꿔주는 것이다. 이것을 Non Null 단언이라고 한다.

 

무조건 타입 단언만을 사용하면 안 되는 이유

type Person = {
    name: string;
    color: string;
}

// 오류 뜨는 경우
let user: Person = {}; // 지금 말고 추후에 추가하고 싶은데.. name과 color가 없다고 오류가 뜨네?
// 정상 작동하는 경우
let user: Person = {} as Person; // user 변수를 Person 타입으로 단언
user.name = 'shawn';
user.color = 'red';

위 코드는 맨 처음 타입 단언의 방법을 설명할 때 예시 코드로 사용했던 코드이다. 우리가 빈 객체를 만들 수 있는 이유도 타입 단언 때문이었고, 타입 객체에 프로퍼티가 없는데도 그 타입을 사용하는 객체에는 프로퍼티를 사용할 수 있는 이유도 타입 단언 때문이었다. 어떻게 보면 타입 단언은 '강제적' 성향을 띠고 있다. 강제적 성향을 띤다는 것은 무언가를 쉽게 조작할 수도 있다는 뜻이다. 

 

타입 단언은 타입스크립트 입장에서 "이건 원래 A 타입이라서 들어올 수 없는데 이렇게 단언해 준다고? 원래는 안 되지만 개발자 말이니까 개발자 말을 우선으로 듣자!"라고 생각하게끔 한다. 그렇기 때문에 개발자 입장에서는 코딩을 쉽게할 수 있는 방법이겠지만, 어찌 보면 타입스크립트의 순정을 거스르고 있는 행위이기도 하다.

 

따라서, 가급적이면 타입 선언 방식으로 타입을 부여하고, 타입 단언이 꼭 필요한 순간에만 타입 단언을 사용하자!

type Person = {
    name?: string;
    color?: string;
}

let user: Person = {}; // 옵셔널 체이닝으로 타입 선언했기 때문에 오류가 발생하지 않음
user.name = 'shawn';
user.color = 'red';

위 코드는 빈 객체를 만들고 싶을 때, 타입 단언이 아닌 타입 선언으로 수정한 방식이다. 프로퍼티에 옵셔널 체이닝을 걸어주면 간단히 해결할 수 있다. (타입에 옵셔널 체이닝이 있는 경우에는 '!'를 항상 기억하자.)

 

1. 타입 단언이란, 타입 선언과 같이 타입 부여 방식 중 하나로 as를 이용해 뒤에서 부여하는 방식이다.
2. 값 as 타입의 형태로 타입 자리에는 반드시 값의 슈퍼 타입 혹은 서브 타입 관계의 타입이 와야 한다.
3. 객체 타입의 모든 프로퍼티에 readonly를 걸어주고 싶다면 as const로 단언해 주자.
4. 객체 타입의 프로퍼티에 옵셔널 체이닝 '?'이 걸려 있어서 undefined과 union 타입이 나온다면 '!'를 써주자.
5. 되도록이면 타입 단언보다는 타입 선언을 사용하자. (선택적 프로퍼티로 선언해주기)