티스토리 뷰

반응형

 

 

이펙티브 타입스크립트 - YES24

타입스크립트는 타입 정보를 지닌 자바스크립트의 상위 집합으로, 자바스크립트의 골치 아픈 문제점들을 해결해 준다. 이 책은 《이펙티브 C++》와 《이펙티브 자바》의 형식을 차용해 타입스

www.yes24.com

 

TS 컴파일러의 독립적인 두 가지 역할

TS 컴파일러의 역할을 큰그림에서 봤을 때 아래와 같이 나타낼 수 있다고 한다.

1. TS/JS를 브라우저에서 동작할 수 있도록 JS로 트랜스파일(transpile) 한다 (버전간 변환도 해줌)
2. 코드의 타입 오류 체크

그리고, 이 두가지는 완전히 독립적으로 동작한다.

이것이 무슨 말이냐면, 실제 TS를 컴파일 해보면 알 수 있는데, 코드 중간에 타입 에러가 있어도 끝까지 트랜스파일을 한다는 것이다. 즉, transpile을 수행하면서, type checking도 독립적으로 한다는 것이다. 이것은 대부분의 컴파일 언어들의 컴파일러들은 중간에 오류가 있으면 그 시점에서 중단하고 에러를 반환하는 것과 대조적인데, 이는 TS가 컴파일할 때, 바이트코드가 아닌 JS로 transpile 하며 이것이 인터프리터 언어인것을 생각하면 전혀 이상하지 않다.

 

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

이미 위에서 언급한 내용이다.

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

 

TS Playground - An online editor for exploring TypeScript and JavaScript

The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.

www.typescriptlang.org

TS 플레이그라운드에서 이러한 것들을 쉽게 확인해볼 수 있다

  • 타입 오류지만, 컴파일러 입장에서의 '경고'와 비슷하다고 보면 좋을 것 같다.
    • 즉, 문제는 있지만 경고만 해주고 결과물을 만들어주는 것이다.
  • 타입 에러가 있더라도 컴파일된 결과물이 나오는 것은 도움이 된다.
    • 마지막엔 저 경고같은 에러들을 다 처리해야겠지만, 나온 결과물을 통해 타입 에러가 없는 파트들이라도 테스트해볼 수 있다. 뭐 그렇다고 기존 컴파일 언어들보다 더 낫다라고 볼 순 없는 것이, 이 언어의 결과물이 바이너리가 아닌 JS라고 생각하면 이러한 특성을 잘 활용한 것 뿐이다. 즉, 결과물 자체가 다르기 때문에 가능한 것이다.
  • 만약 보다 엄격하게 타입 에러를 허용하지 않으려면, tsconfig.json에 noEmitOnError를 설정하자. (optional)

 

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

 

아래 코드는 에러를 반환한다.

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;
  }
}
  • TS의 타입은 '제거 가능(erasable)' 하다.
  • 실제로 JS로 컴파일 되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 제거 된다. 이는 플레이그라운드에서 결과물을 통해 쉽게 확인할 수 있다. .d.ts에 이 정보들이 저장되지만, JS파일에서는 제거되버린다.
  • 즉, instanceof 체크는 런타임에 일어나지만, Rectangle은 타입이기 때문에 런타임 시점에 아무런 역할을 할 수 없다.

shape의 타입을 명확하게 하려면, 런타임에 타입 정보를 유지하는 방법이 필요하다. 

아래와 같이 해보자.

// property check
function calculateArea(shape: Shape) {
  if ('height' in shape) {
    return shape.width * shape.height;
  } else {
    return shape.width * shape.width;
  }
}
// tag style
interface Square {
  kind: 'square';
  width: number;
}

interface Rectangle  {
  kind: 'rectangle';
  width: number;
  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;
  }
}
// 타입을 class로 만들기
// 타입(런타임 접근불가)과 값(런타임 접근 가능)을 둘 다 사용하는 기법
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;
  }
}

 

타입 연산은 런타임에 영향을 주지 않는다.

// 타입 체커를 통과하지만 잘못된 방법
function asNumber(val: number | string): number {
  return val as number;
}

// 변환된 JS 코드 (뭐든 리턴해줘버린다)
function asNumber(val) {
  return val;
}
  • as number의 'as'는 타입 연산이며, 런타임 동작에는 아무런 영향을 미치지 않는다.
  • 런타임의 타입을 체크해야 하고 JS 런타임 연산을 통해 타입 변환을 하자.
function asNumber(val: number | string): number {
  return typeof(val) === 'string' ? Number(val) : val;
}

 

런타입 타입은 선언된 타입과 다를 수 있다.

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default:
      console.log('이것은 dead 코드인가');
  }
}
  • 런타임에서는 : boolean이 제거되기때문에, 컴파일 타임에서 체크가 불가능한 런타임 call은 default 호출이 가능하게끔 만든다. 
  • 순수 JS 였다면, setLightSwitch("ON")로 의도되지 않은 호출도 가능하다.
  • 즉 아래와 같이 사용해버리면, 보장받을 수 없게 된다.
interface LightApiResponse {
  lightSwitchValue: boolean;
}

async funtion setLight() {
  const response = await fetch('/light');
  const result: LgithApiResponse = await reponse.json();
  setLightSwitch(result.lightSwitchValue);
}
  • /light를 요청하면 결과로 LightApiResponse를 반환하라고 선언했지만, 실제로 그렇게 되리라는 보장은 없다.
  • 반환되는 LightSwitchValue가 실제로는 문자열이었다면 런타임에는 setLightSwitch 의 인자로 전달되버린다.
  • 어쨌든, 이처럼 타입스크립트에서는 런타임과 선언된 타입이 맞지 않을 수 있다.
  • 중요한 것은 컴파일 타임과 런 타임을 고려하며 코드를 작성해야 한다는 것이다.

 

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

  • 일반적으로 타입 strict한 언어에서는 타입만 달라도 함수 오버로딩을 지원한다.
  • 하지만, TS에서는 타입과 런타임의 동작이 무관하기 때문에 함수 오버로딩은 불가하다.
// duplicate 에러 발생
function add(a: number, b: number) { return a + b; }
function add(a: string, b: string) { return a + b; }
  • TS가 함수 오버로딩 기능을 지원하기는 하지만, 컴파일 타임의 타입체크로써만 사용된다.
  • 여러개의 선언문을 작성할 수 있지만, 구현체는 오직 하나 뿐이다.
function add(a: number, b: number): number;
function add(a: string, b: string): string;

function add(a, b) { return a + b; }; //a, b는 implicit any type이다. 즉 런타임에서는 다 받아줘버린다

const three = add(1, 2);
const twelve = add('1', '2');

 

타입스크립트 타입은 런타임 성능에 영향을 주지 않는다.

  • 타입과 타입 연산자는 트랜스파일 시점에 제거되기 때문에 런타임의 성능에 아무런 영향을 주지 않는다.
  • '런타임' 오버헤드가 없는 대신, '빌드타임' 오버헤드가 발생한다.

 

요약

  • 코드 생성은 타입 시스템과 무관하다. 타입스크립트 타입은 런타임 동작이나 성능에 영향을 주지 않는다.
  • 타입 오류가 존재하더라도 코드 생성(컴파일)은 가능하다.
  • 타입스크립트 타입은 런타임에 사용할 수 없다. 런타임에 타입을 지정하려면 타입 정보 유지를 위한 별도의 방법이 필요하다. (checking properties, tagged union, class type)
  • 런타임 타입은 선언된 타입과 다를 수 있다. 따라서 여전히 런타임에서의 값에 대한 validation 체크가 필요하다.
반응형

'CS > Language' 카테고리의 다른 글

[TypeScript] Namespace  (1) 2022.04.11
[TypeScript] 구조적 타이핑 (Structural Typing)  (1) 2022.03.26
[TypeScript] type vs interface  (1) 2022.03.22
[TypeScript] 유용한 링크 모음 (WIP)  (0) 2022.03.22
[C Language] C 코딩 스타일  (0) 2016.12.26
댓글