본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

리터럴 타입: 값 자체를 계약으로 삼기

~11 min · primitives, literal-types, narrowing, domain-modeling

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"‘문자열 하나’가 아니라 ‘허용된 바로 그 문자열’이라고 말할 수 있을 때 상태가 선명해져."

값 하나가 타입이 된다

문자열, 숫자, 불리언 리터럴은 그 값 하나만 허용하는 타입이 될 수 있어. type Direction = 'left' | 'right'는 임의의 문자열이 아니라 두 값 가운데 하나만 받는 계약이야. 오타는 컴파일 단계에서 막히고 편집기는 가능한 선택지를 자동 완성해 줘.

이 패턴은 상태, 권한, 명령 이름, 설정 단계처럼 가능한 값이 작고 닫혀 있는 곳에서 빛나. 실행 중에는 평범한 문자열이라 별도 객체나 숫자 매핑이 생기지 않아. 타입 층에서만 선택지를 제한하는 가벼운 열거형인 셈이지.

넓히기와 고정하기

const mode = 'dark'는 재할당할 수 없어 대개 리터럴 타입 'dark'로 추론돼. 객체 속성과 배열 원소는 기본적으로 더 넓게 추론될 수 있어. { mode: 'dark' } as const처럼 고정하면 속성이 읽기 전용이 되고 정확한 리터럴 정보가 남는다.

반대로 앞으로 여러 값이 들어올 변수에는 너무 좁은 리터럴 타입이 방해가 돼. let mode: 'dark' = 'dark'라고 해 두면 light로 바꿀 수 없지. 실제 상태 공간에 맞춰 리터럴 유니언을 설계해야 해.

판별 필드의 뼈대

리터럴 타입은 판별 가능한 유니언의 핵심이야. { kind: 'circle'; radius: number }{ kind: 'square'; side: number }를 합치면 kind 값만 확인해도 컴파일러가 객체 전체의 모양을 좁힐 수 있어. 그냥 kind: string이라면 어느 변형인지 알 수 없으니 이 힘이 사라져.

값 목록에서 타입을 파생해

실행 중 순회할 값 목록과 타입 유니언을 따로 적으면 둘이 어긋날 수 있어. const MODES = ['light', 'dark'] as const를 만들고 type Mode = typeof MODES[number]로 타입을 파생하면 진실의 원천이 하나가 돼. 목록을 바꾸면 타입도 함께 바뀐다.

닫힌 선택지는 리터럴 유니언으로 나타내고, 가능하면 실행값에서 타입을 파생해. 문자열의 편리함과 열거형의 안전을 함께 얻는 가장 TypeScript다운 방식이야.

피파의 고백

상태를 string으로 두면 새 값을 넣기 쉽지만, 그 자유는 오타에도 똑같이 열려 있어. 가능한 값이 이미 정해졌다면 자유가 아니라 무관심이야. 리터럴 유니언은 문을 닫는 대신 열쇠 목록을 편집기에 건네줘.

Code

Literal 타입과 union·typescript
// Literal 타입 — 정확히 한 값 담는 타입.
type IdleOnly = 'idle';

const a: IdleOnly = 'idle';                  // ✅
const b: IdleOnly = 'busy';                  // ❌ Type '"busy"' is not assignable to type '"idle"'

// Literal-type union — 가장 유용한 패턴.
type Status = 'idle' | 'loading' | 'success' | 'error';

function handle(s: Status) {
  // TS 가 정확한 집합 아니까 narrowing 작동.
  if (s === 'idle') return 'waiting';
  if (s === 'loading') return 'spinner';
  if (s === 'success') return 'check';
  return 'cross';                            // 여기서 s 는 'error' 로 narrow
}

handle('idle');                              // ✅
handle('typo');                              // ❌ compile 시점에 잡힘
Widening — 규칙과 escape hatch·typescript
// Widening vs literal 유지 — 3가지 패턴.

const a = 'red';                             // a: 'red'  (유지 — const)
let b = 'red';                               // b: string (widening — let)
const c: 'red' | 'blue' = 'red';             // c: 'red' | 'blue' (명시적 annotation)

// Object literal 이 string property 를 widening.
const config1 = { color: 'red' };            // config1: { color: string }  (widening)
const config2 = { color: 'red' } as const;   // config2: { readonly color: 'red' } (유지)

// Literal-union parameter 가진 함수에 전달할 때 중요.
function setColor(c: 'red' | 'blue') {}

setColor(config1.color);                     // ❌ string 은 'red' | 'blue' 아님
setColor(config2.color);                     // ✅ literal 'red' 괜찮음

// `as const` 는 TS 가 너무 공격적으로 widening 하는 곳의 override.

External links

Exercise

red | yellow | green 리터럴 합집합으로 신호등 상태를 만들고 다음 상태를 반환하는 함수를 작성해. never로 완전성을 검사한 뒤 flashing 상태를 추가해 빠진 분기가 드러나는지 봐.
Hint
완전성 검사는 기본 분기에서 값을 never 변수에 대입한 다음, 도달 불가능 오류를 던지는 순서로 작성해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.