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

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

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

이 두가지 역할은 서로 독립적인 역할인데, 서로 영향을 미치지 않는다는 말을 뜻한다. 타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 않는다. 또한 그 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않는다.

따라서 타입 오류가 있는 코드도 컴파일이 가능하다.

컴파일은 타입 체크와 독립적으로 동작하기 때문에, 타입 오류가 있는 코드도 컴파일이 가능하다.

$ cat test.ts
let x = 'hello';
x = 1234;
$ tsc test.ts // 타입 체크 & 트랜스파일 시작
'1234' 형식은 'string' 형식에 값을 할당할 수 없습니다.

$ cat test.js // 트랜스파일 된 자바스크립트
let x = 'hello';
x = 1234'

타입스크립트의 오류는 경고와 비슷하다. 문제가 될 만한 부분을 알려 주지만, 그렇다고 빌드를 멈추지는 않는다.
타입스크립트는 여전히 컴파일 된 자바스크립트를 생성하기 때문에, 문제가 된 오류를 수정하지 않더라도 애플리케이션의 다른 부분을 테스트 할 수 있다는 장점이 있다.

오류가 있을 때, 컴파일하지 않으려면...

tsconfig.json에 noEmitOnError를 설정하거나 빌드 도구에 동일하게 적용하면 된다!


런타임에는 타입 체크가 불가능하다.

타입스크립트 타입은 런타임에 사용할 수 없다. 실제로 타입스크립트의 타입은 제거기능이 있다. 자바스크립트로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 제거되어 버린다.

런타임에 타입을 지정하려면, 타입 정보 유지를 위한 별도의 방법이 필요하다. 일반적으로는 태그된 유니온과 속성 체크 방법을 사용한다. 또는 클래스 같이 타입스크립트 타입과 런타임 값, 둘 다 제공하는 방법이 있다.

런타임에 타입 정보를 유지

interface Square {
	width: number;
}

interface Rectangle extends Square {
	height: number;
}

type Shape = Square | Rectengle;

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

instanceof 체크는 런타임에 일어나지만 Rectangle은 타입이기 때문에 런타임 시점에 어떤 역할도 할 수 없다.
shape의 타입을 명확하게 하고 싶다면 런타임에 타입 정보를 유지할 필요가 있다.
height 속성이 존재하는지 체크하는 것이다.

function calculateArea(shape: Shape){
	if ('height' in shape){
      	shape; // 타입이 Rectangle
    	return shape.width * shape.height;
    } else{
      	shape; // 타입이 Square
    	return shape.width * shape.width;
    }
}

속성 체크는 런타임에 접근 가능한 값에만 관련되지만, 타입 체커도 shpe의 타입을 Rectangle로 보정해 주기 때문에 오류가 사라진다.


런타임에 접근 가능한 타입 정보를 명시적으로 저장하기 (태그 기법)

interface Square {
  	kind: 'square';
	width: number;
}
interface Rectangle extends Square {
  	kind: 'rectangle';
	height: number;
  	width: number;
}
type Shape = Square | Rectengle;

function calculateArea(shape: Shape){
	if ('height' in shape){
      	shape; // 타입이 Rectangle
    	return shape.width * shape.height;
    } else{
      	shape; // 타입이 Square
    	return shape.width * shape.width;
    }
}

여기서 shape 타입은 태그된 유니온(tagged union)의 한 예이다. 이 기법은 런타임에 타입 정보를 손쉽게 유지할 수 있기 때문에, 타입스크립트에서 흔하게 볼 수 있다.


그것도 아니면 타입과 값을 둘 다 사용해버리자

타입(런타임 접근 불가 값)과 값 (런타임 접근 가능)을 둘 다 사용하는 기법도 있다.
바로 타입을 클래스로 만들면 된다. Square와 Rectangle을 클래스로 만들면 오류를 해결 할 수 있다.

👉 클래스가 뭔지 모른다면..

class Square{
	constructor(public width: number){}
}
class Rectangle extends Square {
	constructore(public width: number, public height: number){
    	square(width);
    }
}
type Shape = Square | Rectengle;

function calculateArea(shape: Shape){
	if (shape instanceof Rectengle){
      	shape; // 타입이 Rectangle
    	return shape.width * shape.height;
    } else{
      	shape; // 타입이 Square
    	return shape.width * shape.width; // 정상
    }
}

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

type Shape = Square | Rectengle; 부분에서 Rectangle은 타입으로 참조되지만, shape iinstanceof Rectengle 부분에서는 값으로 참조가 된다.

profile
개인 이력, 포폴 관리 및 기술 블로그 사이트 👉 https://aimzero-web.vercel.app/

0개의 댓글