"두 세계, 한 파일. Compiler 는 둘 다 읽어. Runtime 은 하나만 봐."
TypeScript 에서 가장 쓸모 있는 단일 정신 모델
이 quest 전체에서 한 가지 아이디어만 가져갈 거면, 이거 가져가: TypeScript 코드는 하나처럼 보이는 두 layer 에 살아. Type layer 는 compiler, 에디터, 그리고 너의 눈이 읽어. Value layer 는 JavaScript runtime 이 실제로 실행하는 거야. 둘은 같은 파일, 같은 줄, 가끔 같은 단어를 공유 — 근데 같은 세계가 아냐, 한쪽에서 작동하는 도구가 다른 쪽에서는 안 돼.
: string 같은 type annotation 은 type layer 에만 존재해. `tsc` 가 JavaScript emit 할 때 사라져. 'hello' 같은 값은 value layer 에 존재해. Compile 후에도 살아남아, runtime 이 필요로 하니까. typeof 같은 keyword 는 두 layer 다 존재해 — 근데 같은 철자로 각자 세계에서 다른 일을 하는 두 operator 야. 초보자는 그 overlap 에 계속 걸려. 해결은 2-layer 정신 모델.
Type layer 전용 vs value layer 전용
Type layer 전용: type alias, interface 선언, type annotation (: number, : User[]), generic parameter (<T>), type-level typeof, keyof, infer, mapped types, conditional types, utility-types 라이브러리 전부. 이것들 중 어느 것도 `.js` 출력에 emit 안 돼.
Value layer (regular JavaScript): 변수, 함수 body, class method, object literal, string concat, if / for, value-level typeof, instanceof, async/await 실행. 다 compile 후에도 변하지 않고 살아남아 (구버전 target 용 down-level 빼고).
두 layer 다 살지만 다른 의미: class 선언 (constructor 값 + 타입 둘 다 만듦), enum (object 값 + 타입 둘 다), namespace (legacy — 둘 다), typeof keyword (value-level 은 'string' 같은 문자열 반환, type-level 은 값의 타입 읽음).
Type erasure — 진짜로 사라지는 것
직접 실험해 봐: Playground 에 TypeScript 파일 붙여넣고 JS 출력 panel 눌러. 사라진 거 다 봐. Annotation 사라졌어. Interface 사라졌어. Generic 꺾쇠 사라졌어. as cast 사라졌어. 심지어 ! non-null assertion 도 사라졌어. 남은 건 runtime program 이고, 방금 자기를 검사한 type system 에 대해 zero 지식이야.
typeof aValue 통해서), 근데 value-layer 코드는 type-layer 타입을 못 읽어 (runtime 에 없으니까). 화살표는 한 방향: 타입은 값을 볼 수 있어, 값은 타입을 못 봐.이 규칙이 코드 쓰는 방식을 바꾸는 이유
경계 규칙이 TypeScript 의 90% gotcha 를 설명해. catch (e) 가 thrower 가 선언한 타입 대신 e: unknown 주는 이유 — throw 가 runtime 인데, throw 의 타입은 살아남지 못해서. if (user instanceof MyInterface) 못 쓰는 이유 — MyInterface 가 runtime 에 존재 안 함, 비교할 게 없어. typeof 가 가끔 이거고 가끔 저것인 이유 — 한 keyword 를 공유하는 두 typeof 가 있으니까. 2-layer 모델 internalize 하면 더 이상 어느 것도 놀랍지 않아.