# 타입스크립트 타입 체커와 관련한 깨달음 1



## 서론

타입스크립트 타입 체커와 관련해서 한가지 사실을 깨달았고, 이렇게 오래 고민한 문제는 오랜만이라 기록으로 남긴다.
그리고 깨달음 1인 이유는 아직 고칠 게 너무 많아서이다.

---

## 문제

```ts
declare function foo(x: string): number;
declare function foo(x: number): string;

function g1() {
  let x: string | number | boolean;
  x = "";
  while (cond) {
    x = foo(x);
    x;
  }
  x;
}
```

이 문제를 이해하기 위해 필요한 배경 지식이 좀 있다.

### 배경 지식: 실제 타입

타입스크립트에서의 변수는 타입을 2개 가진다.
하나는 선언 타입, 또 다른 하나는 실제 타입이다.
선언 타입은 변수에 할당할 때 쓰이는 타입이고 실제 타입은 변수를 사용할 때 쓰이는 타입이다.

이 구분은 아래와 같은 코드를 처리하기 위해서 쓰인다.

```ts
function stringOnly(s: string) {}

let x: string | number;
x = "foo";

stringOnly(x);
```

x가 `string | number`로 선언되었기 때문에 많은 언어에서는 타입 에러가 났을 것이다.
하지만 타입스크립트는 실제 타입 / 선언 타입이 분리되어있기 때문에 위 코드는 정상적으로 컴파일된다.

타입스크립트를 재구현하는 입장에서는 좀 골치가 아프지만, 이건 실제 타입울 저장해놓으면 되기 때문에 큰 문제는 아니다.
문제는 반복문과 섞였을 때 발생한다.

```ts
declare function foo(x: string): number;
declare function foo(x: number): string;

function g1() {
  let x: string | number | boolean;
  x = "";
  while (cond) {
    x = foo(x);
    x;
  }
  x;
}
```

문제의 코드를 다시 보자.
while 루프가 시작할 때 `x`는 `string`이다.
따라서 `foo(x)`는 첫번째 함수를 호출하고 반환값은 `number`가 된다.
그럼 이제 x는 `number`이다.

2번째 루프에서 `x`는 `number`일 것이다.
`foo(x)`의 타입은 `string`이 된다.

따라서, 루프 안에서의 타입을 적어보면

```ts
declare function foo(x: string): number;
declare function foo(x: number): string;

function g1() {
  let x: string | number | boolean;
  x = "";
  while (cond) {
    // x: string, number 중 하나
    x = foo(x);
    // x: number, string 중 하나
    x;
  }
  x;
}
```

이렇게 된다.
순서는 동순이다. (복호동순)

이걸 처리하는 게 생각보다 복잡하다.
그래도 어찌어찌 처리는 했었는데, 일단 기존 처리 방식과 그 문제점부터 얘기하겠다.

## 기존 처리 방식

난 `while` 문을 처리할 때 검증 모드를 끄고 `x`의 실제 타입을 `string`으로 놓고 루프를 검증하고, 해석하면서 나온 정보 (`facts`, 여기서는 `x = number`)을 다음 루프에 넣도록 했다.
그리고 루프를 해석해봤는데 새로운 정보를 만들어내지 않으면 검증 모드를 켜고 정보(`facts`)를 종합한 뒤 루프를 검사한다.
검증 모드를 끄는 건 변수 타입을 정확하게 알 수 없어서 에러가 나기 때문이다.

이렇게 해도 작동은 한다.
문제는 느리다는 점이다.
루프문이 타입 정보(`facts`)를 만들어낼지 안 만들어낼지 알 수 없기 때문에 모든 루프를 2번 이상 해석해야하고, 반복문이 중첩되면 해석 횟수가 n1 \* n2 \* n3처럼 늘어난다.
빠를 수가 없는 구조다.
물론 러스트니까 이래도 `tsc` 보단 함참 빠르다.
근데 언어가 아무리 러스트여도 `O(n1 * n2 * n3)` 꼴의 시간 복잡도는 부답스럽다.

### tsc

그래서 타입스크립트 소스 코드를 봤다.
근데 `checkWhileStatement` 소스 코드를 보니끼 한번만 검증하더라.

```ts
function checkWhileStatement(node: WhileStatement) {
  // Grammar checking
  checkGrammarStatementInAmbientContext(node);

  checkTruthinessExpression(node.expression);
  checkSourceElement(node.statement);
}
```

어떻게 이렇게 하는지 한참 고민했다
개발할 때 고민 잘 안 하는 스타일인데 이건 이게 어떻게 가능한지 직관적으로 생각이 안 났다.

## 깨달음

그러다가 생각난 게 있는데, 루프를 여러 번 해석할 필요가 없다는 점이 그것이다.
어차피 검증 모드를 켜고 에러를 생성하는 건 마지막 루프 딱 한번이다.
그리고 그때 루프에 넣어주는 x의 타입은 `string | number`이다.

그렇다면 `x = foo(x);` 만 여러번 해석해도 된다.
`x = foo(x)`의 첫번째 해석해서 `x = number`라는 걸 뽑아내는 건 동일하다.
그런데 루프문 해석기에 정보를 넘겨주는 대신에 `x = number`로 놓고 `x = foo(x)`를 다시 해석할 수 있다.
그러면 `x = string`이라는 정보가 나오므로, `x = string | number`이라는 걸 한번에 알 수 있다.

물론 다른 언어에선 x가 `string`인 경우와 `number`인 경우를 나눠야할 수도 있지만, 공식 `tsc`는 그런 걸 지원하지 않는다.
이 새로운 처리 방식은 `tsc`의 할당 표현식 처리 함수가 트램폴린이라는 개념을 쓰는 걸 보고 알게 된 것이다.
쉽게 말하면 `tsc`가 이런 트릭을 쓰는 것 같았다.
(코드가 너무 방대한 관계로 제대로 분석하진 않았다.)
따라서 타입스크립트 구현자 입장에서는 이 정도면 충분하다.

이것 때문에 느낀 점이 있는데, 제대로 포팅을 할 거라면 원본 소스를 어느 정도는 봐야한다는 걸 느꼈다.
난 원래 테스트 코드만 복붙해와서 테스트 하나씩 고쳐가는 식으로 아예 다시 구현을 해버린다.
주된 이유는 그게 재밌어서인데... 이제 재미만으로 하는 프로젝트가 아니니까 재미 없더라도 참고 원본 소스를 참고할 생각이다.

