8.1 타입 단언 (Type Assertion) 8.1.1 타입 단언이란? 1) as 단언 2) <> 단언8.1.2 타입 단언 규칙8.1.3 const 단언8.1.4 Non-Null 단언8.1.5 다중 단언8.1.6 타입 단언의 사용 목적과 주의할 점1) try catch 문에서 error가 unknown으로 추론 될 때2) DOM 요소에 접근할 때 3) 객체를 먼저 선언하고 나중에 속성을 추가하려 할 때4) 함수의 반환 값으로 union 타입을 사용할 때8.2 타입 가드 (Type Guard)8.2.1 타입 가드란?8.2.2 타입 가드 적용 방법1) typeof2) Array.isArray()3) in4) instanceof5) 리터럴6) null과 undefined7) 사용자 정의 타입 가드8) 서로소 유니온 타입
8.1 타입 단언 (Type Assertion)
8.1.1 타입 단언이란?
타입 단언은 타입스크립트가 타입 추론을 예상과 다르게 추론했을 때 사용합니다. 개발자가 해당 값의 타입을 확신할 때 명시적으로 지정해 컴파일러에게 “이 값은 이 타입이야” 라고 알려주는 것과 같습니다. 대표적인 타입 단언 방법 두 가지를 알아보겠습니다.
1) as 단언
기본적인
as
연산자를 사용해 Value as <Type>
형식으로 타입 단언을 할 수 있습니다. 위의 코드에서
one
변수의 타입이 unknown이지만 타입 단언을 사용해 string 타입으로 단언해주면 one
변수의 문자열 길이를 출력할 수 있습니다.2) <> 단언
<Type>Value
형식을 사용해 타입 단언을 할 수 있습니다. 먼저 살펴봤던 예제 코드를
<>
연산자를 사용해 <string>one
형태로 타입을 지정할 수 있습니다. 하지만 React의 .tsx 파일에선 구문 분석에 어려움을 초래할 수 있으므로, 타입 단언을 위해 <>
대신 as
연산자를 사용하는 것을 권장합니다.타입 캐스팅(Type Casting)과 타입 단언은 어떤 점이 다를까요?
다른 언어에서 사용되는 비슷한 개념인 타입 캐스팅(Type Casting)와 비교해 보면 사용 방식과 특정 값이 특정 타입임을 컴파일러에 알려주는 점에서 비슷하지만 작동 방식에서 차이가 있습니다.
타입 단언은 런타임에는 아무런 영향을 주지 않기 때문에 실행 에러는 미리 방지하지 못하고, 오직 컴파일 타임에서만 타입을 변화시켜 타입 에러만 해결할 수 있고, 실제 데이터의 타입을 변환시키지 않습니다.
타입 캐스팅은 런타임과 컴파일 타임에서 실제 데이터의 타입을 변경시킨다는 점에서 타입 단언과 차이를 알아볼 수 있습니다.
8.1.2 타입 단언 규칙
이번 절에서는 타입 단언의 규칙에 대해 알아보겠습니다. 타입 단언을 사용할 때는 값과 타입 간의 관계가 중요한데, 이는 부모-자식 관계로 설명될 수 있습니다.
Value as <Type>
형식을 가질 때, Value는 Type의 슈퍼 타입(부모 타입)이거나 서브 타입(자식 타입)이어야 합니다. 따라서 이 두 가지 조건 중 하나는 반드시 만족해야 합니다.위의 코드에서는 Value와 Type의 관계가 서로 호환되지 않아 에러가 발생합니다. 이를 호환 가능한 타입으로 고치면 아래와 같습니다.
하지만 기존 타입과 호환되지 않는 타입으로 단언할 필요가 있는 상황이 발생합니다. 이 경우 다중 단언을 사용해 나타낼 수 있습니다. 다중 단언은 목차 8.1.5에서 자세히 알아보도록 하겠습니다.
8.1.3 const 단언
as const
는 객체, 배열, 원시 타입 등에 대해 readonly
와 리터럴 타입
을 동시에 적용할 수 있는 방법입니다. 모든 속성이 readonly
가 되고, 배열이 불변(Immutable)이 되기 때문에 속성을 변경하거나 배열에 요소를 추가하거나 제거하는 것이 불가능합니다.위의 코드에서는 배열
arr
을 [100, "hello"]로 초기화하고 as const
를 사용해 arr
배열을 읽기 전용으로 만들었습니다. 이에 따라 배열은 변경할 수 없는 상태가 되며, 따라서 배열의 속성을 변경하려고 하면 에러가 발생합니다.위의 코드에서는
one
의 타입이 as const로 단언했기 때문에 리터럴 타입
이 되는 걸 확인할 수 있습니다.8.1.4 Non-Null 단언
Non-Null 연산자는 변수가 null이 아님을 보장하는 단언 방법입니다.
! 연산자
를 사용하면 값이 null 또는 undefined 가 아니라는 것을 컴파일러에 알릴 수 있습니다.위의 코드를 보면 매개변수
param
이 null이거나 undefined일 수도 있기 때문에 string의 메서드인 slice()를 사용할 수 없다는 에러가 발생합니다.다음과 같이
param
이 null이나 undefined가 아닌 것이 확실하다면 값 뒤에 ! 연산자
를 붙여 오류를 해결할 수 있습니다.8.1.5 다중 단언
다중 단언은 먼저 unknown이나 any 타입으로 단언한 후, 다시 원하는 타입으로 단언하는 방법입니다. 8.1.2절에서 살펴본 예제에 다중 단언을 적용해 아래와 같이 강제 형 변환을 할 수 있습니다.
위 코드를 살펴보면 먼저 hello를 unknown으로 단언하고, number 타입으로 단언합니다. 이렇게 하면 타입스크립트는
one
변수를 number로 인식해 toFixed()
메서드를 사용해도 에러가 나타나지 않습니다. 하지만 실제로 hello가 number 타입으로 변하는 게 아니기 때문에 런타임에 toFixed()
메서드를 호출하려고 하면 에러가 발생합니다. 따라서 필요한 상황에 맞게 사용하는 것을 권장합니다.8.1.6 타입 단언의 사용 목적과 주의할 점
이번 절에선 타입 단언은 언제 사용하면 좋은지 알아보고 사용 시 주의해야 할 점에 대해 알아보도록 하겠습니다.
1) try catch 문에서 error가 unknown으로 추론 될 때
위의 코드를 보면 TypeScript에서는 try-catch 문의 catch 블록에서 잡을 수 있는 오류가 어떤 타입이 될지 알 수 없기 때문에 오류 객체
error
의 타입을 unknown으로 추론해 에러가 발생합니다.위의 코드처럼
error
를 Error
타입으로 단언하면 error
의 타입이 unknown이 아닌 Error
로 인식되어 관련 기능을 수행할 수 있습니다.2) DOM 요소에 접근할 때
위의 코드를 보면 DOM 요소에 접근할 때 해당 요소가 실제로 존재하는지 확인할 수 없기 때문에 해당 요소의 속성에 접근하려고 하면 오류가 발생합니다. 이런 경우 아래 코드와 같이 타입 단언을 사용해 더 구체적으로 요소의 타입을 지정해 줄 수 있습니다.
첫 번째
img
변수의 경우 ! 연산자
를 사용하여 null이 될 가능성이 없다고 타입스크립트에게 알려줌으로써 null이 아니라는 것을 단언하고 있습니다.두 번째
myImg
변수의 경우, as
를 사용하여 더 구체적 타입인 HTMLImageElement
타입으로 단언함으로써 속성에 접근할 수 있게 되었습니다.세 번째
nextImg
변수는 <> 연산자
를 사용해 타입을 단언했습니다.하지만 이렇게 단언한 후에도 실제로 요소가 존재하는지 확인하지 않고 속성에 접근하면 여전히 런타임 에러가 발생할 수 있습니다.
3) 객체를 먼저 선언하고 나중에 속성을 추가하려 할 때
타입 단언은 객체를 먼저 선언하고 나중에 속성을 추가하려 할 때 사용할 수 있습니다. 이 경우
as
연산자를 사용해 변수를 먼저 선언하고, 나중에 필요한 속성을 추가할 수 있습니다.주의할 점은 실수로 필요한 속성을 누락하거나 잘못된 타입의 값을 할당하는 등의 오류를 발생시킬 수 있으므로 안정성 있는 타입 검사를 위해서 타입 단언보다 타입 선언을 권장하고 있습니다.
타입 선언을 사용하면 할당되는 값이 선언된 타입을 만족하는지 먼저 검사해 오류를 발생시키지만, 타입 단언은 타입의 일부 속성만 사용하거나 속성을 추가한다 해도 오류를 발생시키지 않습니다. 다음 예시를 살펴보며 타입 선언과 타입 단언의 차이를 알아보겠습니다.
위의 코드에서 타입 선언한 변수
scar
은 객체에 속성을 정의하지 않았기 때문에 오류가 발생합니다. 반면 타입 단언을 사용한 simba
는 빈 객체를 Animal
타입으로 단언했기 때문에 오류를 발생시키지 않습니다.위의 코드에서 타입 선언과 달리 타입 단언을 사용한
simba
는 객체의 일부 속성만 사용하거나, 속성을 추가해도 에러를 발생시키지 않습니다.4) 함수의 반환 값으로 union 타입을 사용할 때
위의 코드에서
addOrConcat
함수는 매개변수 c의 값에 따라 다른 타입의 결과를 반환합니다. 이 함수의 반환 값은 number 또는 string인 union 타입이기 때문에, myVal
변수에 대해 타입 단언을 하지 않으면 타입스크립트에서는 에러가 발생합니다.이 문제를 해결하기 위해서는
myVal
변수에 대해 string으로 타입 단언을 통해 반환 값의 타입을 명확하게 지정해야 합니다. 그러나 wrongVal
변수의 경우에는 다른 문제가 발생합니다. wrongVal
변수는 실제로는 문자열을 반환하지만, number로 타입 단언을 하게 되면 컴파일러는 이를 number 타입으로 인식합니다. 그러나, 실제 실행 시점에서는 string 타입으로 인식됩니다. wrongVal
의 타입을 출력하면 string이 출력되는 것을 확인할 수 있습니다. toFixed(2)
메서드를 wrongVal
에 적용하려고 하면, 실제 wrongVal
의 값은 문자열이므로 toFixed()
메서드가 존재하지 않아 실행 시점에서 에러를 발생합니다.따라서 타입 단언은 컴파일러에 특정 값의 타입을 알려주는 역할만 하며, 실제 값의 존재 여부나 타입을 보장하지는 않습니다. 그래서 변수의 실제 값이 존재하는지, 예상한 타입이 맞는지 확인하려면 실행 시점에서 타입 가드를 사용해야 합니다.
8.2 타입 가드 (Type Guard)
8.2.1 타입 가드란?
타입 가드는 타입의 안정성을 보장하기 위해 변수나 객체의 타입을 좁혀나가는 것을 의미합니다. 타입 가드가 필요한 이유를 이전 목차에서 문제가 있었던 코드와 함께 알아보겠습니다.
이전 목차에서 살펴봤던 위의 코드는 타입 단언을 잘못 사용하여 실제 값의 타입과 단언한 타입이 일치하지 않아 런타임에서 에러가 발생한다는 문제를 가지고 있습니다. 이 문제를 해결하기 위해 아래와 같이 타입 가드를 적용할 수 있습니다.
typeof
를 사용하여 myVal
과 wrongVal
의 실제 타입을 체크하고, 이에 따라 적절한 로직을 실행하도록 수정했습니다. 이렇게 타입을 좁혀 실제 타입에 따라 다른 로직을 실행하도록 타입 가드를 적용할 수 있습니다.타입 가드를 통해 더 정확한 타입을 명시할 수 있게 되므로 코드의 명확성이 향상되고, 버그 발생 가능성이 감소하며, 타입스크립트의 자동 완성 기능도 더욱 효과적으로 사용할 수 있게 됩니다. 타입 가드를 적용하는 방법으로는
typeof
, Array.isArray()
, instanceof
, in
, 리터럴
, null
, undefined
, 사용자 정의
등이 있으며 다음 목차에서 적용 방법을 하나씩 알아보도록 하겠습니다.8.2.2 타입 가드 적용 방법
1) typeof
변수의 타입을 문자열로 반환하는
typeof
를 이용해 타입 가드를 구현할 수 있습니다.위의 코드에서 if 문에
typeof
를 사용해 x의 타입이 문자열일 경우에만 해당 명령문이 실행되도록 타입 가드를 적용했습니다. if 문 블록 안에서의 x는 문자열로 간주하기 때문에 문자열에 사용되는 toUpperCase()
메서드는 정상적으로 사용이 가능하지만, 숫자형에 사용하는 메서드인 toFixed()
는 사용할 수 없고 오류가 발생하게 됩니다. 또, if 문을 벗어나게 되면 x의 타입이 명확하지 않아 문자열 또는 숫자형 메서드를 사용했을 때 오류가 발생하게 됩니다.2) Array.isArray()
전달된 값이 배열인지를 판단하는
Array.isArray()
를 통해 타입 가드를 구현할 수 있습니다.findArray
함수는 숫자형 배열 또는 단일 숫자인 매개변수 x를 사용합니다. Array.isArray()
를 사용해 x가 배열인지 확인합니다. findArray
함수에서 x가 배열일 경우 if 문의 조건인 Array.isArray(x)
는 true로 if 문 안의 명령이 실행되게 됩니다. 이처럼 x가 배열일 경우에만 명령을 실행하도록 타입 가드를 만들 수 있습니다.3) in
객체가 특정 프로퍼티를 가지고 있는지 확인하는데 사용되는
in
을 이용해 타입 가드를 구현할 수 있습니다.위의 코드에는
Shoes
와 Muffler
라는 두 가지 인터페이스가 있습니다. Shoes
에는 number 유형의 단일 프로퍼티 size가 있고, Muffler
에는 number 유형의 단일 프로퍼티 length가 있습니다. itemSize
함수는 Shoes
또는 Muffler
객체일 수 있습니다. 함수 내부에서 in
연산자를 사용하여 length 속성이 item에 있는지 확인하는 타입 가드를 적용합니다. length가 있으면 item이 Muffler
, length가 없으면 item이 Shoes
임을 의미하며 이에 맞는 명령문이 실행됩니다. 4) instanceof
객체가 특정 클래스의 인스턴스인지 확인하는 용도로 사용되는
instanceof
를 통해 타입 가드를 구현할 수 있습니다. 위의 코드에는
Book
과 Food
라는 두 개의 클래스가 있으며 각각 고유한 메서드를 가지고 있습니다. action
함수는 Book
또는 Food
타입을 가지며 함수 내부에서 instanceof
를 사용하여 'x'가 Book
클래스의 인스턴스인지 확인합니다. Book
클래스의 인스턴스가 맞다면 read
메서드를 호출하고, 아니라면 Food
라고 가정하고 eat
메서드를 호출합니다. 이렇게 instanceof
를 사용하여 함수 내의 유형을 좁히는 타입 가드를 만들 수 있습니다.5) 리터럴
특정 리터럴 값을 기반으로하는 타입 가드를 구현할 수 있습니다.
위의 코드에서
Feeling
타입은 문자열 리터럴 결합('happy' | 'angry' | 'sad')을 사용하여 생성되므로, Feeling
타입으로 지정된 변수는 3가지 문자열 값 중 하나만 취할 수 있습니다. modeStatus
함수는 Feeling
타입으로 지정된 feeling 매개변수의 값을 평가하기 위해 switch 문을 사용합니다. feeling의 값이 케이스와 일치하면 특정 메시지를 콘솔창에 출력하도록 리터럴 값을 사용한 타입 가드를 적용했습니다.6) null과 undefined
null
과 undefined
를 사용해 타입 가드를 구현할 수 있습니다.위 코드에서
A
라는 타입은 숫자, null, undefined로 지정됩니다. A 타입으로 지정된 변수는 세 가지 값 중 하나를 가집니다. whatIsX
함수는 A
타입의 매개변수 x를 받습니다. 함수 내에서 x의 범위를 좁히기 위해 null과 undefined를 사용한 동등성 검사 조건문으로 타입 가드를 만들 수 있습니다. 7) 사용자 정의 타입 가드
타입스크립트에서는 사용자 정의 타입 가드 함수를 만들어 사용할 수 있습니다. 사용자 정의 타입 가드 함수는 리턴값에 is 연산자를 사용해 타입을 명시합니다. 이름을 입력하지 않는 사람을 찾아내는 사용자 정의 타입 가드 함수를 만들어보며 적용 방법을 알아보도록 하겠습니다.
우선 사용자의 정보를 담는
User
인터페이스를 정의하겠습니다.User
인터페이스는 사용자의 이름(name)과 나이(age)를 프로퍼티로 가지며, name은 string 타입 또는 null을 가질 수 있습니다. 즉, 이름을 입력하지 않은 사용자는 name 프로퍼티가 null인 User
객체입니다.그럼 이제 이름을 입력하지 않은 사용자를 찾아내는 사용자 정의 타입 가드 함수를 만들어 보겠습니다.
위 코드에서
hasNoName
함수는 User
타입의 객체를 받아서, 그 객체의 name 프로퍼티가 null인지를 검사하는 사용자 정의 타입 가드 함수입니다. 이 함수는 user is User & { name: null } 형태의 반환 타입을 가지며, 이는 user의 name 프로퍼티가 null이면 true를 반환한다는 것을 의미합니다. 이렇게 타입 가드를 통해 타입스크립트는 if (hasNoName(user)) 문이 true일 때 user의 name 프로퍼티가 null임을, false일 때 user의 name 프로퍼티가 string임을 알아낼 수 있습니다.8) 서로소 유니온 타입
서로소 유니온 타입은 타입 가드를 할 때 직관적으로 객체 타입을 정의하는 방법입니다. 교집합이 없는 타입으로만 만든 유니온 타입이라고 할 수 있습니다.
세 객체를 합쳐
User
라는 union 타입을 만들었습니다. 하지만 이대로
User
를 사용할 경우, 주석이 없다면 각 타입 가드가 어떠한 타입을 말하는지 확인하기 어렵습니다. 의도와는 다르게 추가로 각 타입의 프로퍼티를 확인하며 사용해야 합니다.이때 tag 프로퍼티를 추가하여 타입 좁히기를 직관적으로 변경할 수 있습니다. tag 프로퍼티는 리터럴 타입으로 정의 되어 있기 때문에 1가지의 값만 가지게 되므로 각 타입은 독립적으로 교집합 없이 존재하게 됩니다.
객체에 선택적 프로퍼티가 정의되어 있는 경우 옵셔널 체이닝이나 조건문을 추가하여 해결하는 것보다 타입을 분리하여 서로소 유니온 타입으로 만들어 해결하는 게 직관적이고 안전합니다.