C.W.K.
Stream
Lesson 05 of 05 · published

Literal 타입: `'red'` 이 자기 타입이 될 때

~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
"타입은 카테고리 아냐. 받아들일 정확한 값 집합이야. `'red'` 자체가 완벽하게 좋은 타입."

사고 전환

대부분 프로그래머가 타입이 카테고리인 정적 타입 언어로 타이핑 배워 — "이건 number," "이건 string." TypeScript 도 그거 할 수 있지만, 더 강한 것도 할 수 있어: 타입이 단일 값일 수 있어. type Status = 'idle' 은 정확히 한 값, 문자열 `'idle'` 만 담는 타입. 다른 거 다 실패.

Literal 타입을 union 과 결합하면 enum 같은 거 — 근데 더 유연하고 compile 시점에 erased. type Status = 'idle' | 'loading' | 'success' | 'error' 는 status 필드의 허용 값 정확한 집합 묘사. 다른 거 전달하면 compiler 가 막아.

Inference 가 literal vs widened 고르는 법

TypeScript 가 literal 타입 줄지 widened parent 줄지 결정하는 규칙:

  • const x = 'hi'x: 'hi' 추론 (literal — `const` 못 바뀜).
  • let x = 'hi'x: string 추론 (widened — `let` 은 어떤 string 도 될 수).
  • const x: 'hi' = 'hi' 명시 annotate — widening 안 됐을 거지만 literal 타입 유지.
  • const arr = ['hi']arr: string[] 추론 (배열 element 는 default 로 widening).
  • const arr = ['hi'] as constarr: readonly ['hi'] 추론 (as const 가 widening 과 mutability 둘 다 죽임).

이 widening 행동이 as const 가 존재하는 이유 — TypeScript 가 그러지 않으면 generalize 할 곳에서 literal 타입 유지하는 override knob.

Literal 타입 3가지 카테고리

String literal: type Color = 'red' | 'green' | 'blue'. 가장 흔한 형태. Status 필드, tag 이름, union 의 kind discriminator 에 쓰여.

Number literal: type Quadrant = 1 | 2 | 3 | 4. 덜 흔하지만 제한된 숫자 도메인에 유용. HTTP status code 가 자연스러운 적합: type Status = 200 | 404 | 500.

Boolean literal: type True = true. 단독으론 드물지만, conditional type 과 nail down 하고 싶은 flag 에 나타나.

도메인 모델링 전환: `string` 같은 primitive 타입으로 데이터 묘사 멈추고, 실제로 나타나는 값의 정확한 집합으로 묘사 시작. Compiler 가 그러면 너의 코드가 실제로 일어날 수 있는 case 만 처리한다고 증명 가능.

Literal 타입이 narrowing 을 강하게 만들어

Literal 타입을 일찍 배우는 가장 큰 이유: 나중에 narrowing 트랙이 명백하게 느껴지게 만들어. `status` 가 `'idle' | 'loading' | 'done'` 타입일 때, if (status === 'idle') 가능하고 그 분기 안에서 TypeScript 가 `status` 는 literal `'idle'` 이라고 알아 — 그리고 다른 분기는 남은 멤버로 narrow. 이게 discriminated union, exhaustive switch, 트랙 6 과 7 의 대부분 패턴의 기초.

피파의 고백

cwkPippa frontend 는 literal-type union 위에 살아. `BrainName` 은 'claude' | 'codex' | 'gemini' | 'ollama'. `ReasoningLevel` 은 'minimal' | 'low' | 'medium' | 'high' | 'ultra'. 모든 async hook 의 status 필드는 literal union. 어느 것도 enum 아냐 — 그냥 literal-type union, compile 시점에 erase 돼서 runtime 은 plain string 봐. Type system 이 constraint 잡고, runtime 은 가벼움 유지.

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

Literal-type union 으로 신호등 모델링: type Light = 'red' | 'yellow' | 'green'. Cycle 의 다음 light 반환하는 next(light: Light): Light 함수 써. Switch 에 exhaustiveness 처리하는 default case 포함 (이전 lesson 의 never 트릭 써). 이제 Light union 에 'flashing' 추가하고 exhaustive 체크가 작동하는 거 봐.
Hint
Exhaustive default 는: default: const _: never = light; throw new Error(Unreachable: ${_});. 'flashing' 추가 전엔 compiler 가 행복. 추가 후엔 never 에 assignment 실패 — 어느 switch 가 이제 incomplete 인지 정확히 말해줘.

Progress

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

댓글 0

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

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