본문으로 건너뛰기

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

타입스크립트 컴파일러는 두 가지 역할을 수행한다.

  • 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일한다.
  • 코드의 타입 오류를 체크한다.

이 두 가지는 서로 완벽히 독립적이다.

타입 오류가 있는 코드도 컴파일이 가능합니다​

타입 오류가 존재하더라도 코드 생성(컴파일)은 가능하다.

코드에 오류가 있을 때 “컴파일에 문제가 있다”고 말하는 경우를 보았을 것이다. 그러나 이는 기술적으로 틀린 말이다. 엄밀히 말하면 오직 코드 생성만이 ‘컴파일’이라고 할 수 있기 때문이다. 작성한 타입스크립트가 유효한 자바스크립트라면 타입스크립트 컴파일러는 컴파일을 해낸다. 그러므로 코드에 오류가 있을 때 “타입 체크에 문제가 있다”고 말하는 것이 더 정확한 표현이다.

런타임에는 타입 체크가 불가능합니다​

interface Square {
width: number;
}

interface Rectangle extends Square {
height: number;
}

type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
if (shape instanceof Rectangle) {
// ERROR: 'Rectangle'은(는) 형식만 참조하지만, 여기서는 값으로 사용되고 있습니다.
return shape.width * shape.height;
//ERROR: 'Shape' 형식에 'height' 속성이 없습니다. 'Square' 형식에 'height' 속성이 없습니다.
} else {
return shape.width * shape.width;
}
}

instanceof 체크는 런타임에 일어나지만, Rectangle 은 타입이기 때문에 런타임 시점에 아무런 역할을 할 수 없다.

앞의 코드에서 다루고 있는 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;
return shape.width * shape.height;
} else {
shape;
return shape.width * shape.width;
}
}

타입 정보를 유지하는 또 다른 방법으로는 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 ‘태그’ 기법이다.

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;
return shape.width * shape.height;
} else {
shape;
return shape.width * shape.width;
}
}

Shape 타입은 태그된 유니온의 한 예시이다.

타입과 값을 둘 다 사용하는 기법도 있다. 타입을 클래스로 만드는 것이다.

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) {
shape;
return shape.width * shape.height;
} else {
shape;
return shape.width * shape.width;
}
}

인터페이스는 타입으로만 사용 가능하지만, Rectangle 을 클래스로 선언하면 타입과 값으로 모두 사용할 수 있으므로 오류가 없다.

런타임 타입은 선언된 타입과 다를 수 있습니다​

타입스크립트에서는 런타임 타입과 선언된 타입이 맞지 않을 수 있다.

타입스크립트 타입으로는 함수를 오버로드할 수 없습니다​

타입스크립트에서는 타입과 런타임의 동작이 무관하기 때문에, 함수 오버로딩은 불가능하다.

function add(a: number, b: number) {
// ERROR: 중복된 함수 구현입니다.
return a + b;
}

function add(a: number, b: number) {
// ERROR: 중복된 함수 구현입니다.
return a + b;
}

타입스크립트가 함수 오버로딩 기능을 지원하기는 하지만, 온전히 타입 수준에서만 동작한다. 하나의 함수에 대해 여러 개의 선언문을 작성할 수 있지만, 구현체는 오직 하나뿐이다.

function add(a: number, b: number): number;

function add(a: string, b: string): string;

function add(a, b) {
return a + b;
}

const three = add(1, 2); // number

const twelve = add('1', '2'); // string

add 에 대한 처음 두 개의 선언문은 타입 정보를 제공할 뿐이다. 이 두 선언문은 타입스크립트가 자바스크립트로 변환되면서 제거되며, 구현체만 남게 된다.

타입스크립트 타입은 런타임 성능에 영향을 주지 않습니다​

타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에, 런타임의 성능에 아무런 영향을 주지 않는다. 타입스크립트의 정적 타입은 실제로 비용이 전혀 들지 않는다.

  • 타입스크립트 컴파일러는 ‘빌드타임’ 오버헤드가 있다.
  • 제너레이터 함수가 ES5 타깃으로 컴파일되려면, 타입스크립트 컴파일러는 호환성을 위한 특정 헬퍼 코드를 추가할 것이다. 이런 경우가 제너레이터의 호환성을 위한 오버헤드 또는 성능을 위한 네이티브 구현체 선택의 문제이다.
노트
{
// number로 값을 narrowing하는 잘못된 방법
// as number 가 자바스크립트로 컴파일하면 사라져서 의도하지 않은 동작이 될 수 있음
function asNumber(val: number | string): number {
return val as number;
}
}
{
// 런타임시에 값을 정제하는 방법
// 이런 방식이 좋은지 의문이 듦
function asNumber(val: number | string): number {
return typeof val === 'string' ? Number(val) : val;
}
}