아이템 03. 코드 생성과 타입이 관계없음을 이해하기

아이템 03. 코드 생성과 타입이 관계없음을 이해하기

아이템 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. 런타임 시 속성 체크
  2. 태그 기법 (타입 정보 보유)
  3. 클래스 사용

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. 타입 오류가 존재해도 트랜스파일(코드 생성)은 가능하다.



  1. 책에서 말하고 있는 ‘타입’ vs ‘값’에 대한 차이를 이해가 필요하다.
  2. 런타임 시에 타입이 제거된다. 타입 관련 로직들에 대한 주의가 필요하다.
  • 의미 없는 코드 작성(예를 들어, 트랜스파일 후 제거되는 코드 작성)할 여지가 많이 있을 것 같다.
  • 런타임 시에 타입은 아무런 영향도 끼칠 수 없다는 것을 기억해야 한다.