메뉴판에 없는 건 주문 못 합니다
카페에서 음료를 주문할 때, 메뉴판에 없는 걸 달라고 하면 어떻게 될까요? "그런 메뉴는 없습니다"라는 답이 돌아올 겁니다. 메뉴판이 있으면 주문 가능한 선택지가 명확히 제한됩니다.
TypeScript의 리터럴 타입(Literal Type)이 딱 이런 역할을 합니다. 변수에 담을 수 있는 값을 딱 몇 가지로 고정하는 것입니다.
리터럴 타입이란
이전 챕터에서 const로 변수를 선언하면 타입이 좁아진다고 잠깐 언급했습니다.
const direction = "left"; // 타입: "left" (string이 아님!)
"left"라는 값 자체가 타입이 됩니다. 이것이 리터럴 타입입니다.
이걸 활용하면 특정 값만 허용하는 타입을 만들 수 있습니다.
let align: "left" | "right" | "center";align = "left"; // OKalign = "center"; // OKalign = "top"; // 에러! Type '"top"' is not assignable to type '"left" | "right" | "center"'.
"left", "right", "center" 세 가지 값만 허용합니다. 다른 문자열은 들어올 수 없습니다.
유니언 타입과 함께 쓰는 리터럴
| 기호는 "또는"을 의미합니다. 이것을 유니언 타입이라고 합니다.
type Direction = "left" | "right" | "up" | "down";type Status = "pending" | "success" | "error";type Coin = 10 | 50 | 100 | 500;
리터럴 타입은 문자열뿐 아니라 숫자에도 쓸 수 있습니다.
function move(direction: Direction) { console.log(`${direction} 방향으로 이동합니다.`);}move("left"); // OKmove("up"); // OKmove("jump"); // 에러! Argument of type '"jump"' is not assignable to parameter of type 'Direction'.
함수 매개변수에 리터럴 타입을 쓰면 잘못된 값이 들어오는 것을 컴파일 시점에 막을 수 있습니다.
타입 별칭으로 이름 붙이기
type 키워드를 사용하면 복잡한 타입에 이름을 붙일 수 있습니다.
type Align = "left" | "right" | "center";type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";type DayOfWeek = "월" | "화" | "수" | "목" | "금" | "토" | "일";
이름을 붙여두면 여러 곳에서 재사용할 수 있습니다.
function setTextAlign(element: HTMLElement, align: Align) { element.style.textAlign = align;}function fetchData(url: string, method: HttpMethod) { // ...}
열거형 — 상수를 하나로 묶기
리터럴 타입과 비슷한 용도로 쓰이지만 다른 문법이 있습니다. 바로 열거형(Enum)입니다.
enum Direction { Left, Right, Up, Down,}
Direction.Left, Direction.Right처럼 점으로 접근합니다.
function move(direction: Direction) { console.log(direction);}move(Direction.Left); // OKmove(Direction.Up); // OKmove(0); // OK (숫자 열거형은 숫자도 허용됨 — 주의!)
기본적으로 enum은 숫자 값을 가집니다. Left는 0, Right는 1, Up은 2, Down은 3입니다. 직접 값을 지정할 수도 있습니다.
enum Direction { Left = "LEFT", Right = "RIGHT", Up = "UP", Down = "DOWN",}move(Direction.Left); // OKmove("LEFT"); // 에러! 문자열 열거형은 값으로 직접 접근 불가
문자열 열거형은 숫자 열거형보다 안전합니다. 숫자 열거형은 숫자를 직접 넘겨도 에러가 나지 않기 때문입니다.
const enum — 더 가볍게
일반 enum은 컴파일 후 JavaScript 코드에 객체가 생성됩니다. const enum을 사용하면 컴파일 시 값으로 인라인 처리되어 런타임 객체가 생기지 않습니다.
const enum Direction { Left = "LEFT", Right = "RIGHT", Up = "UP", Down = "DOWN",}const dir = Direction.Left;// 컴파일 후: const dir = "LEFT";
번들 크기를 줄이고 싶을 때 const enum을 사용합니다. 다만 외부 모듈에서 가져다 쓰거나 동적으로 접근할 때는 제한이 있습니다.
리터럴 타입 vs enum 비교
| 구분 | 리터럴 타입 | enum |
|---|---|---|
| 문법 | "left" | "right" |
enum Direction { Left, Right } |
| 값 접근 | "left" 직접 사용 |
Direction.Left |
| JS 변환 | 타입만 있고 코드 없음 | 객체가 생성됨 |
| 자동완성 | 지원 | 지원 |
| 추천 상황 | 간단한 선택지 | 명확한 도메인 상수 그룹 |
실무에서는 enum보다 리터럴 타입과 type 별칭 조합을 더 많이 사용하는 추세입니다. enum은 컴파일 결과에 JavaScript 코드가 추가되기 때문입니다. 하지만 enum은 가독성이 좋고 의미가 명확해서 팀에 따라 여전히 많이 씁니다.
실전 예제
실제 코드에서 어떻게 쓰이는지 확인해보겠습니다.
type ButtonVariant = "primary" | "secondary" | "danger";type ButtonSize = "small" | "medium" | "large";interface ButtonProps { variant: ButtonVariant; size: ButtonSize; label: string;}function renderButton(props: ButtonProps) { const { variant, size, label } = props; return `<button class="${variant} ${size}">${label}</button>`;}renderButton({ variant: "primary", size: "medium", label: "확인" }); // OKrenderButton({ variant: "big", size: "medium", label: "확인" }); // 에러!
"big"은 ButtonVariant에 없는 값이므로 에러가 납니다. 오타나 실수를 컴파일 시점에 잡아주는 것입니다.
다음 챕터에서는 타입 시스템에서 특수한 역할을 하는 세 가지 타입, any, unknown, never를 살펴보겠습니다. 이 세 가지를 이해하면 TypeScript가 진짜 왜 any를 싫어하는지 알게 됩니다.