"컴파일러가 믿는 것과 실행 환경이 받는 것은 같은 값이지만, 같은 세계는 아니야."
두 층을 먼저 갈라
TypeScript를 읽을 때는 언제나 두 층을 구별해야 해. 첫째는 컴파일 시점이야. 타입 검사기가 소스 코드를 읽고 값의 가능한 모양과 호출 관계를 확인하는 자리이지. 둘째는 실행 시점이야. 타입이 모두 지워진 JavaScript가 실제 입력을 받고 네트워크를 호출하며 파일을 읽는 자리야.
const n: number = 3에서 : number는 첫 번째 층에만 있어. 출력된 JavaScript에는 const n = 3만 남아. 그래서 타입 오류가 없다는 말은 실행 중 모든 일이 안전하다는 뜻이 아니야. 서버가 문자열을 보냈는데 코드가 숫자라고 단언하면 컴파일러는 그 거짓말을 알아낼 수 없어.
타입 단언은 변환이 아니야
value as User는 값을 User로 바꾸지 않아. 컴파일러에게 ‘이 값은 User라고 내가 책임질게’라고 말할 뿐이야. 속성을 채우지도 않고, 잘못된 값을 거부하지도 않아. 외부 JSON을 단언 하나로 통과시키면 타입 검사를 얻은 게 아니라 검사할 기회를 포기한 셈이지.
경계에서는 실행 중 검증이 필요해. 직접 속성과 타입을 확인하거나 Zod 같은 스키마 도구로 파싱한 뒤, 검증을 통과한 결과에만 안전한 타입을 부여해야 해. 안쪽 코드에서는 TypeScript가 계약을 이어 가고, 바깥에서 들어오는 값은 검증기가 문을 지키는 구조가 가장 단단해.
같은 철자가 두 층에서 다른 일을 하기도 해
값 위치의 typeof value는 실행 중 'string' 같은 문자열을 돌려주지만, 타입 위치의 typeof value는 선언된 값의 정적 타입을 읽어. keyof, infer, 매핑 타입과 조건부 타입은 타입 층에만 있고, instanceof는 실행 중 생성자 관계를 검사해.
그래서 user instanceof MyInterface는 쓸 수 없어. interface는 출력에서 사라져 실행 중 비교할 값이 없기 때문이야. 반면 class는 생성자 값과 인스턴스 타입을 함께 만들어 instanceof에 사용할 수 있어.
직접 지워지는 것을 확인해
Playground의 JavaScript 출력 창을 열어 타입 주석, interface, 제네릭 꺾쇠, as 단언, 비-null 단언이 사라지는지 봐. 남은 JavaScript가 런타임 프로그램이고, 그 프로그램에는 앞서 검사를 수행한 타입 시스템의 정보가 없어.
지워지는 것과 남는 것
interface, type alias, 제네릭 매개변수, 대부분의 타입 표기는 결과 JavaScript에서 사라져. 반면 class, enum처럼 실행값도 만드는 일부 문법은 남을 수 있어. ‘TypeScript 문법’이라는 이유만으로 모두 사라진다고 외우면 틀려. 그 문법이 타입 정보만 표현하는지, 실행할 값을 만들기도 하는지를 물어야 해.
unknown, 타입 술어, 선언 파일, API 응답 처리까지 한 줄로 이어져.오류를 읽는 순서
컴파일 오류를 만나면 먼저 어느 층의 문제인지 가려. ‘속성이 없다’거나 ‘대입할 수 없다’면 타입 층의 모양이 충돌한 거야. 실행 중 undefined에 접근했다면 실제 값이 계약을 어긴 거고, 입력 경계나 좁히기가 부족했을 가능성이 커. 두 층을 섞지 않으면 디버깅이 훨씬 짧아져.