Ch 05. 타입 단언과 as const
타입 단언(type assertion)은 개발자가 컴파일러보다 타입을 더 잘 안다고 선언하는 방법입니다. 강력하지만 남용하면 TypeScript의 보호막을 스스로 제거하는 꼴이 됩니다. as const는 반대로 컴파일러가 타입을 더 정확하게 추론하도록 돕는 도구입니다.
as 단언의 올바른 사용법
as는 타입 간 변환이 아닙니다. 컴파일러가 타입을 알 수 없거나 잘못 추론할 때 개발자가 정확한 타입을 알려주는 것입니다.
// 파일: src/assertions/as-basic.ts// DOM 조작 — getElementById는 HTMLElement | null을 반환하지만// id가 "canvas"인 요소가 HTMLCanvasElement임을 개발자가 확신할 때const canvas = document.getElementById("canvas") as HTMLCanvasElement;const ctx = canvas.getContext("2d"); // HTMLCanvasElement의 메서드 사용 가능// JSON 파싱 — 결과가 unknown이므로 타입을 지정해야 할 때interface Config { apiUrl: string; timeout: number;}const rawConfig = JSON.parse(localStorage.getItem("config") ?? "{}") as Config;
as 단언은 TypeScript가 컴파일 타임에만 동작합니다. 런타임에는 아무 검증도 하지 않습니다. 따라서 값이 실제로 해당 타입인지 보장할 수 없을 때는 타입 가드를 사용해야 합니다.
as 단언의 남용 패턴과 문제점
// 파일: src/assertions/as-abuse.ts// 나쁜 예시 — any로 만들어서 모든 검사를 우회function badExample(data: unknown) { const user = data as any; // 모든 타입 검사 무력화 console.log(user.name.toUpperCase()); // 런타임 에러 가능}// 나쁜 예시 — 관련 없는 타입으로 단언 (이중 단언)const number = 42 as unknown as string; // 컴파일은 통과하지만 의미 없음console.log(number.toUpperCase()); // 런타임 에러// 좋은 예시 — 타입 가드로 대체function goodExample(data: unknown) { if ( typeof data === "object" && data !== null && "name" in data && typeof (data as { name: unknown }).name === "string" ) { const user = data as { name: string }; console.log(user.name.toUpperCase()); // 안전 }}
이중 단언(double assertion)인 value as unknown as OtherType은 TypeScript의 안전장치를 완전히 우회합니다. 정말 불가피한 경우에만 사용하고, 주석으로 이유를 명시해야 합니다.
as const — 리터럴 타입 고정
as const를 붙이면 값의 타입이 가능한 한 좁은 리터럴 타입으로 고정됩니다. 객체의 모든 속성이 readonly가 되고, 배열이 읽기 전용 튜플이 됩니다.
// 파일: src/assertions/as-const.ts// as const 없이const config1 = { endpoint: "/api/users", method: "GET", timeout: 3000,};// config1.method의 타입: string (넓은 타입)// config1.timeout의 타입: number// as const 사용const config2 = { endpoint: "/api/users", method: "GET", timeout: 3000,} as const;// config2.method의 타입: "GET" (리터럴 타입)// config2.timeout의 타입: 3000 (리터럴 타입)// 모든 속성이 readonly
as const로 enum 대체하기
enum 대신 as const 객체를 사용하는 패턴이 실전에서 많이 쓰입니다.
// 파일: src/assertions/const-enum.ts// enum 방식enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT",}// as const 방식 (권장)const Direction = { Up: "UP", Down: "DOWN", Left: "LEFT", Right: "RIGHT",} as const;// as const 객체에서 값 타입 추출type Direction = (typeof Direction)[keyof typeof Direction];// "UP" | "DOWN" | "LEFT" | "RIGHT"function move(direction: Direction) { console.log(`이동: ${direction}`);}move(Direction.Up); // 정상move("UP"); // 정상 — 리터럴 타입도 허용// move("up"); // 오류 — 소문자는 타입 불일치
as const 방식은 enum과 달리 컴파일 후 객체 그대로 남아 있어 tree-shaking이 가능하고, 일반 JavaScript 객체처럼 사용할 수 있습니다.
배열에 as const 적용하기
// 파일: src/assertions/const-array.ts// as const 없이 — string[]const permissions1 = ["read", "write", "delete"];// permissions1의 타입: string[]// as const — readonly 튜플const permissions2 = ["read", "write", "delete"] as const;// permissions2의 타입: readonly ["read", "write", "delete"]type Permission = (typeof permissions2)[number];// "read" | "write" | "delete"function hasPermission(user: { permissions: Permission[] }, action: Permission): boolean { return user.permissions.includes(action);}// 설정 배열을 타입으로 활용하는 패턴const ROUTES = [ { path: "/", name: "home" }, { path: "/about", name: "about" }, { path: "/users", name: "users" },] as const;type RouteName = (typeof ROUTES)[number]["name"];// "home" | "about" | "users"
non-null 단언 연산자 !
!는 값이 null 또는 undefined가 아님을 단언하는 특수 연산자입니다.
// 파일: src/assertions/non-null.tsconst input = document.getElementById("username") as HTMLInputElement;// ! 없이if (input.value !== null) { console.log(input.value.trim());}// ! 사용 — null이 아님을 확신할 때console.log(input!.value.trim());// 속성 접근 시에도 사용 가능interface Config { database?: { host: string; };}const config: Config = { database: { host: "localhost" } };console.log(config.database!.host); // database가 항상 있다고 확신할 때
non-null 단언도 런타임 검사를 하지 않습니다. 실제로 null일 경우 TypeError가 발생합니다. 옵셔널 체이닝(?.)이나 null 검사를 먼저 고려하고, 그래도 단언이 필요할 때만 사용합니다.
단언 사용 결정 기준
| 상황 | 권장 방법 |
|---|---|
| 런타임에 실제로 타입을 확인해야 함 | 타입 가드 (is, instanceof, typeof) |
| 컴파일러보다 확실히 더 많이 알 때 | as 단언 |
| null/undefined 아님을 확신할 때 | ! 또는 as T (단, 주의해서 사용) |
| 값이 변하지 않을 상수 | as const |
| 유니온에서 특정 타입 강제 | as 단언 (타입이 서로 겹칠 때만) |
타입 단언은 TypeScript와의 협력이 아닌 명령입니다. as const는 협력입니다. 가능하면 as const로 정확한 타입을 확보하고, as 단언은 불가피한 경우에만 최소한으로 사용하는 것이 좋습니다.