아이템 3. 코드 생성과 타입이 관계없음을 이해하기 #
타입스크립트는 두 가지 역할(트랜스파일, 컴파일)을 수행한다. #
1. 최신 타입스크립트/자바스크립트를 (브라우저에서 동작할 수 있도록) 구버전의 자바스크립트로 트랜스파일(transpile)한다.
2. 코드의 타입 오류를 체크한다.
트랜스파일, 타입 체크는 완벽히 독립적이다.
- 트랜스파일이 잘못되어도 타입 체크는 실행될 수 있다.
- 타입 체크가 잘못되어도 트랜스파일은 실행될 수 있다.
타입 오류가 있는 코드도 컴파일(트랜스파일)이 가능하다. #
» cat test.ts
let x = 'hello';
x = 1234;
» tsc test.ts
test.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'.
2 x = 1234;
~
Found 1 error in test.ts:2
» cat test.js
var x = 'hello';
x = 1234;
»
타입스크립트에서 (타입 체크의)오류는 C, 자바에서의 경고(warning)와 비슷하다.
noEmitOnError
옵션을 통해 오류가 있을 때 컴파일 결과물이 생성되지 않도록 설정할 수 있다.- 기본 값 :
false
(= 오류가 있어도 컴파일 결과물을 생성한다.)
- 기본 값 :
런타임에는 타입 체크가 불가능하다. #
타입스크립트의 타입은 ‘제거 가능(erasable)‘하다.
- 자바스크립트로 트랜스파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 제거된다.
따라서, 아래와 같은 코드는 의도한대로 실행되지 않는다.
/** File : test.ts */
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if(shape instanceof Rectangle) {
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
» tsc test.ts
test.ts:10:25 - error TS2693: 'Rectangle' only refers to a type, but is being used as a value here.
10 if(shape instanceof Rectangle) {
~~~~~~~~~
test.ts:11:36 - error TS2339: Property 'height' does not exist on type 'Shape'.
Property 'height' does not exist on type 'Square'.
11 return shape.width * shape.height;
~~~~~~
Found 2 errors in the same file, starting at: test.ts:10
» cat test.js
function calculateArea(shape) {
if (shape instanceof Rectangle) {
return shape.width * shape.height;
}
else {
return shape.width * shape.width;
}
}
타입은 모두 제거되었다.
런타임에 ‘타입 정보’를 유지하기 위해서는 아래와 같은 방법을 사용할 수 있다.
- 런타임 시 속성 체크
- 태그 기법 (타입 정보 보유)
- 클래스 사용
1. 런타임 시 속성 체크
function calculateArea(shape: Shape) {
if('height' in shape) {
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
» tsc test.ts
» cat test.js
function calculateArea(shape) {
if ('height' in shape) {
return shape.width * shape.height;
}
else {
return shape.width * shape.width;
}
}
2. 태그 기법 (타입 정보 보유)
interface Square {
kind: 'square';
width: number;
}
interface Rectangle extends Square {
kind: 'rectangle';
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
if(shape.kind === 'rectangle') {
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
3. 클래스 사용
인터페이스는 ‘타입’으로만 사용 가능하지만, 클래스는 ‘타입’, ‘값’으로 모두 사용 가능하다.
즉, 타입(런타임 접근 불가)와 값(런타임 접근 가능)을 둘 다 사용하는 기법
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) {
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
‘타입 연산’은 런타임에 영향을 주지 않는다. #
(아래 예시) 변환된 .js
에서는 타입에 대한 코드가 모두 제거되었다.
즉 런타임 시에 아무런 영향을 주지 않는다.
function asNumber(val: number | string): number {
return val as number;
}
» tsc test.ts
» cat test.js
function asNumber(val) {
return val;
}
(의도한대로 작성하기 위해서는) 아래와 같이 작성할 수 있다.
function asNumber(val: number | string): number {
return typeof(val) === 'string' ? Number(val) : val;
}
» tsc test.js
» cat test.js
function asNumber(val) {
return typeof (val) === 'string' ? Number(val) : val;
}
런타임 타입은 선언된 타입과 다를 수 있다. #
아래 코드의 default
케이스는 실행될 수 있다.
(
default
케이스에 대해서) (일반적으로) 타입스크립트는 실행되지 못하는 죽은(dead)코드를 찾아내지만, 여기서는 strict 모드를 설정하더라도 찾아내지 못한다.
function setLightSwitch(value: boolean) {
switch (value) {
case true:
turnLightOn();
break;
case false:
turhLightOff();
break;
default:
console.log("???");
}
}
트랜스파일 후 런타임 시에 value: boolean
에서 boolean
은 제거된다. 따라서 아래와 같이 호출될 수도 있다.
setLightSwitch("ON");
즉, 선언된 타입과 런타임 타입이 맞지 않을 수 있다. (이 부분을 명심해야 한다.)
타입스크립트 타입으로는 함수를 ‘오버로드’할 수 없다. #
선언된 타입은 트랜스파일 후에는 모두 제거되기 때문에 당연한 결과일 것이다.
아래 예시를 살펴보자. 트랜스파일 후 선언된 타입은 모두 제거되었다.
function add(a: number, b: number) {
console.log(a + b);
}
function add(a: string, b: string) {
console.log(a + b);
}
» tsc test.js
» cat test.js
function add(a, b) {
console.log(a + b);
}
function add(a, b) {
console.log(a + b);
}
타입스크립트가 함수 오버로딩 기능을 지원하는 것은 ‘타입’ 수준에서만 의미하는 것이다. (…?)
예를 들면 아래와 같이 선언문은 여러 개를 작성할 수 있지만, 구현체는 오직 하나뿐이다. (…?)
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a, b) {
return a + b;
}
» tsc test.js
» cat test.js
function add(a, b) {
return a + b;
}
add 에 대한 두 개의 선언문은 타입 정보를 제공할 뿐이다.
선언문은 자바스크립트로 트랜스파일 될 때 제거된다. (구현체만 남는다.)
타입스크립트 타입은 런타임 성능에 영향을 주지 않는다. #
타입은 자바스크립트로 변환되는 시점에 모두 제거되기 때문에, 런타임에서는 (성능을 포함하여)아무런 영향을 주지 않는다.
‘런타임’ 오버헤드(성능 저하)가 없는 대신, ‘빌드타임’에 대한 오버헤드는 존재한다.
- 타입스크립트 팀은 컴파일러 성능을 매우 중요하게 생각한다. (= ‘빌드타임’에 대해 중요하게 생각한다.)
- ‘빌드타임’에 대한 오버헤드가 너무 커질 경우, 타입 체크는 하지 않고 트랜스파일만 진행할 수도 있다. (transpile only)
요약 #
1. 타입은 런타임 동작 시에 어떠한 영향도 주지 않는다. (런타임 동작 시에 사용할 수도 없다.)
2. 타입 오류가 존재해도 트랜스파일(코드 생성)은 가능하다.
- 책에서 말하고 있는 ‘타입’ vs ‘값’에 대한 차이를 이해가 필요하다.
- 런타임 시에 타입이 제거된다. 타입 관련 로직들에 대한 주의가 필요하다.
- 의미 없는 코드 작성(예를 들어, 트랜스파일 후 제거되는 코드 작성)할 여지가 많이 있을 것 같다.
- 런타임 시에 타입은 아무런 영향도 끼칠 수 없다는 것을 기억해야 한다.