타입스트립트 컴파일러는 두 가지 역할을 수행한다.
- 최신 TS/JS 코드를 브라우저에서 동작할 수 있도록 구버전의 JS로 트랜스파일 한다.
- 코드의 타입 오류를 체크한다.
이 두 가지는 서로 완벽히 독립적으로 동작한다. 즉, TS가 JS로 변환될 때 코드 내의 타입에는 영향을 주지 않으며 JS의 실행 시점(런타임)에도 타입은 영향을 미치지 않는다.
타입 오류가 있는 코드도 컴파일이 가능하다
컴파일은 타입 체크와 독립적으로 동작하기에 타입 오류가 있는 코드도 컴파일이 가능하다.
let x = 'hello';
x = 1234;
// '1234' 형식은 'string' 형식에 할당할 수 없다
타입 체크와 컴파일이 동시에 이뤄지는 C나 자바 같은 언어와는 달리 Typescript는 문제가 될 만한 부분을 알려주지만, 빌드는 멈추지 않는다.
타입 오류가 있는 데도 컴파일이된다는 모습에 TS가 이상해 보일 수 있지만, 코드에 오류가 있더라도 컴파일된 산출물이 나오는 것이 실제 도움이 된다.
웹 애플리케이션 개발시 문제가 발생했다고 가정해보자. TS는 컴파일된 산출물을 생성하기 때문에, 문제가 된 오류를 수정하지 않더라도 다른 부분을 테스트할 수 있다.
만약 오류가 있을 때 컴파일하지 않으려면, tsconfig.json에 noEmitOnError를 설정하거나 빌드 도구에 동일하게 적용하면된다.
런타임에는 타입 체크가 불가능하다
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
// Rectangle은 형식만 참조하지만, 값으로 사용하고 있으며
// 런타임에는 Shape 형식에 height 속성이 없다.
return shape.width * shape.height;
}
else {
return shape.width * shape.width;
}
}
instanceof 체크는 런타임에 일어나지만, Rectangle은 타입기이 때문에 런타임 시점에 아무런 역할을 할 수 없다.
타입스크립트의 타입은 '제거 가능' 하며, JS로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 제거되어 버린다.
위의 코드에서 shape 타입을 명확하게 하려면 런타임에 타입 정보를 유지해야한다. 첫 번째 방법은 height 속성이 존재하는지 체크하는 것이다.
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if ('height' in shape) {
shape; // 타입이 Rectangle
return shape.width * shape.height;
}
else {
shape
return shape.width * shape.width;
}
}
속성 체크는 런타임에 접근 가능한 값에만 관련되지만, 타입 체커 역시 shape의 타입을 Rectangle로 보정 해주기 때문에 오류가 사라진다.
또 다른 방법으로는 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 '태그 기법'이 있다.
interface Square {
kind: 'square';
width: number;
}
interface Rectangle {
kind: 'rectangle';
height: number;
width: number
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape.kind === 'rectangle') {
shape; // 타입이 Rectangle
return shape.width * shape.height;
}
else {
shape
return shape.width * shape.width;
}
}
태그 기법은 런타임에 타입 정보를 쉽게 유지할 수 있기 때문에 TS에서 쉽게 볼 수 있다.
타입과 값을 둘 다 사용하는 기법도 있으며, 이렇게 하려면 타입을 클래스로 만들면 된다.
class Square {
constructor(public width: number) {}
}
class Rectangle extends Square {
constructor(public width: number, public height: number) {
super(width);
}
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
// Rectangle은 형식만 참조하지만, 값으로 사용하고 있으며
// 런타임에는 Shape 형식에 height 속성이 없다.
return shape.width * shape.height;
}
else {
return shape.width * shape.width;
}
}
인터페이스는 타입으로만 사용 가능하지만, Rectangle을 클래스로 선언하면 타입과 값으로 모두 사용할 수 있으므로 오류가 없다. shap instanceof Rectangle 부분에서는 값으로 참조되기에 런타임에도 가능하다.
타입 연산은 런타임에 영향을 주지 않습니다
string 또는 number 타입인 값을 항상 number로 정제하는 경우를 가정해 보자. 타입 체커는 통과하지만 잘못된 코드이다.
function asNumber(val: number | string): number {
return val as number;
}
코드는 아무런 정제 과정이 없다. asNumber는 타입 연산이며 런타임 동작에는 아무런 영향을 미치지 않는다. 값을 정제하기 위해서는 런타임의 타입을 체크해야 하며 JS 연산을 통해 반환해야 한다.
function asNumber(val: number | string): number {
return typeof(val) === 'string' ? Number(val) : val;
}
as number는 '타입 단언문' 이기 때문에 런타임에는 유효하지 않는다.
런타임 타입은 선언된 타입과 다를 수 있다
function setLightSwitch(value: boolean) {
switch (value) {
case true:
turnLightOn();
break;
case false:
turnLightOff();
break;
default:
console.log('실행되지 않을까봐 걱정되네;;;');
}
}
TS는 일반적으로는 실행되지 않는 코드를 찾아내지만, 위의 코드에서는 strict를 설정해도 찾아내지 못한다. 그렇다면 default 코드가 실행되는 경우는 어떤 경우일까?
: boolean은 타입 선언문이라 때문에 런타임에서 제거된다. 그렇기 때문에 인수에 boolean이 아닌 값이 와도 실행에는 문제가 되지 않는다.
순수 TS에서도 default가 실행되는 경우가 있다.
interface LightApiResponse {
lightSwitchValue: boolean;
}
async function setLight() {
const response = await fetch('/light');
const result = LisghtApiResponse = await response,json();
setLightSwitch(result.lightSwitchValue);
}
/light를 요청하면 그 결과로 LightApiResponse를 반환할 것이라 예상하지만, 실제로 그렇게 될 것이라는 보장은 없다.
TS에서 런타임 타입과 선언된 타입이 다를 수 있으며 선언된 타입이 언제든지 달리질 수 있다는 것을 명심해야 한다.
타입스크립트 타입으로는 함수를 오버로딩할 수 없다
C++ 같은 언어는 동일한 이름에 매개변수만 다른 여러 버전의 함수를 허용하는 함수 오버로딩이 있다. 하지만 TS에서는 타입과 런타임의 동작이 무관하기에 함수 오버로딩은 불가능하다.
놀라운 점은 TS에서도 함수 오버로딩을 지원한다. 하지만 온전히 타입 수준에서만 동작한다. 하나의 함수에 대해 여러개의 선언문을 작성할 수 있지만, 구현체는 오직 하나뿐이다.
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a, b) {
return a + b;
}
타입스크립트 타입은 런타임 성능에 영향을 주지 않는다
타입과 타입 연산자는 JS 변환 시점에 제거되기 때문에, 런타임의 성능에 아무런 영향을 주지 않는다. TS의 정적 타입은 실제로 비용이 들어가지 않기 때문이다.
정리
1. 코드 생성은 타입 시스템과 무관하며 타입스크립트 타입은 런타임 동작이나 성능에 영향을 주지 않는다.
2. 타입 오류가 존재해도 코드 생성(컴파일)은 가능하다.
3. 타입스크립트 타입은 런타임에 사용할 수 없으며 런타임에 타입을 지정하려면 일반적으로 태그된 유니온과 속성 체크 방법을 사용하거나, 클래스 같이 타입스크립트 타입과 런타임 값 둘다 제공하는 방법을 이용한다.
'TypeScript > 이펙티브 타입스크립트' 카테고리의 다른 글
아이템2 타입스크립트 설정 이해하기 (0) | 2024.09.03 |
---|---|
Typescript와 Javascript의 관계 이해하기 (0) | 2024.08.26 |