Skip to main content

Command Palette

Search for a command to run...

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

Updated
3 min read

서론

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


문제

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개 가진다. 하나는 선언 타입, 또 다른 하나는 실제 타입이다. 선언 타입은 변수에 할당할 때 쓰이는 타입이고 실제 타입은 변수를 사용할 때 쓰이는 타입이다.

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

function stringOnly(s: string) {}

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

stringOnly(x);

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

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

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 루프가 시작할 때 xstring이다. 따라서 foo(x)는 첫번째 함수를 호출하고 반환값은 number가 된다. 그럼 이제 x는 number이다.

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

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

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 소스 코드를 보니끼 한번만 검증하더라.

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가 이런 트릭을 쓰는 것 같았다. (코드가 너무 방대한 관계로 제대로 분석하진 않았다.) 따라서 타입스크립트 구현자 입장에서는 이 정도면 충분하다.

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

More from this blog

한국의 학벌에 대한 생각

내 블로그의 제목이 kdy1: The way I think 인만큼 앞으로는 내 생각을 더 자주 올리려고 한다. 한국 기준으로, 학벌은 사람을 볼 때 꽤나 유용한 지표이지만, 절대적이지는 않다. 경험적인 얘기일 뿐이지만, 성균관대학교 자퇴생으로서 느낀 것들이 몇 가지 있다. 대학까지 간 사람의 학벌은 학습 능력 x 성실함 에 대체로 비례한다. 그래서 의미가

Apr 3, 20261 min read

인간 지능에 대한 메모장

최종 업데이트: 2026/03/15 지능의 유전 현재 인류 기준으로, 고지능자는 고지능 유전자가 많이 겹친 사람이다. 지능의 유전엔 X 염색체가 매우 중요한 역할을 한다. 그리고 이게 남자와 여자의 지능 분포 차이를 만든다. 극상위권에 여자가 거의 없는 이유가 이것이다. 고지능 X 염색체가 여자한테서 발현되려면 2개가 있어야 한다. 이는 인간의 생

Mar 15, 20262 min read

Ai 코딩 팁 2 (한국어)

발표 자료: https://gamma.app/docs/AI--2a52e7tk3eb1ch1 AI 활용법 관련해서 간단하게 발표를 했다. 발표 자료 앞쪽은 전에 블로그에 올린 글이랑 같은 내용이다. 이 글에서는 기존 글에서 다루지 않은 내용들을 다루겠다. 에러 메시지 및 로깅 구체적 타입 및 스키마 활용 any 타입은 사람에게도 위험하지만, AI에게는 더 위험하다. 마찬가지로, JSON.parse처럼 아무 제약 없는 파싱 느슨한 인터페이스 ...

Jan 30, 20265 min read

kdy1: The way I think

233 posts