리터럴 타입은 값이 정확히 하나인 타입입니다. "up"은 유일한 멤버가 문자열 "up"인 타입이고, 404는 유일한 멤버가 숫자 404인 타입입니다. 그 자체로는 쓸모가 많지 않습니다. 유니언으로 묶으면 정해진 값의 집합만 받고 다른 것은 받지 않는 변수를 만들 수 있습니다.
@ts-expect-error 주석이 없으면 마지막 호출은 컴파일 오류(TS2345)입니다. 주석이 있으면 프로그램이 컴파일되고, 호출도 그대로 실행되어 moving north by 1을 출력합니다. 리터럴 타입은 컴파일러를 위해서만 존재하고, 런타임에 값은 평범한 문자열입니다.
문자열, 숫자, 불리언 리터럴
어떤 문자열, 숫자, bigint, 불리언 값이든 타입으로 쓸 수 있습니다. 그러면 컴파일러는 정확히 그 값만 받아들입니다.
let method: "GET" = "GET";
let code: 404 = 404;
let ok: true = true;
let big: 10n = 10n;
type Port = 80 | 443 | 8080;
const port: Port = 443;
boolean 자체는 유니언 true | false일 뿐입니다. 그래서 if (flag)로 boolean을 좁히면 else 분기에는 false가 남습니다.
| 리터럴 타입 | 허용하는 값 | 넓은 타입 |
|---|---|---|
"GET" | 문자열 "GET"만 | string |
404 | 숫자 404만 | number |
10n | bigint 10만 | bigint |
true | true만 | boolean |
리터럴 유니언
흔한 용도는 허용되는 모든 값을 나열한 유니언입니다. 함수 안에서 값을 확인하면 컴파일러가 유니언을 좁히므로, 각 분기는 자신이 어떤 값을 갖는지 정확히 압니다.
문자열 리터럴 유니언은 TypeScript에서 enum 대신 흔히 쓰는 방법입니다. 런타임 비용이 없고, 값은 로그로 찍거나 JSON으로 보낼 수 있는 평범한 문자열이며, 오타는 컴파일 오류가 됩니다. 장단점은 enum에서 더 다룹니다.
넓히기: let과 const
TypeScript는 리터럴에서 타입을 추론할 때 값이 바뀔 수 있는지 살펴봅니다. const 변수는 다시 대입할 수 없으므로 리터럴 타입을 유지합니다. let 변수는 다시 대입할 수 있으므로 타입이 일반적인 타입으로 넓어집니다.
에디터에서 각 이름 위에 마우스를 올려 추론된 타입을 확인해 보세요. 특정 값만 담는 let을 원한다면 표기하세요: let mode: "light" | "dark" = "light".
객체 속성이 넓어지는 이유
객체 리터럴의 속성은 변경 가능하므로, 객체를 const에 저장해도 속성은 넓어집니다. 리터럴 타입을 뜻하지 않게 마주치는 가장 흔한 경우입니다.
컴파일러는 이렇게 보고합니다.
index.ts(7,15): error TS2345: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
나중에 코드가 req.method = "DELETE"를 실행할 수도 있으므로 req는 { url: string; method: string }으로 추론됩니다. 해결 방법은 세 가지입니다.
네 번째 방법은 satisfies입니다. 객체를 타입에 대해 검사하면서도 속성의 리터럴 타입을 유지합니다.
as const
as const는 const 단언입니다. 표현식 뒤에 붙이면 컴파일러가 가능한 가장 좁은 타입을 추론합니다.
- 문자열, 숫자, 불리언 값은 리터럴 타입을 유지합니다
- 객체 속성은
readonly가 됩니다 - 배열 리터럴은 길이가 고정된
readonly튜플이 됩니다
이 단언은 컴파일 시점에만 존재합니다. 출력된 JavaScript는 as const만 빠진 같은 객체 리터럴이므로, 런타임에 다른 코드가 값을 바꾸는 것을 막지 못합니다. 런타임 보장이 필요하면 Object.freeze도 호출하세요.
as const 배열에서 유니언 타입 만들기
허용되는 값을 런타임에 순회할 수 있는 배열 하나에 두고, 그 배열에서 유니언 타입을 끌어내는 패턴이 자주 쓰입니다. (typeof arr)[number]는 "arr의 아무 요소의 타입"이라는 뜻입니다.
as const가 없으면 ROLES는 string[]이고 Role은 그냥 string입니다. isRole에서 readonly string[]으로 캐스팅하는 이유는, 리터럴 튜플의 includes는 그 리터럴만 받는데 이 함수의 목적은 리터럴이 아닐 수도 있는 문자열을 검사하는 것이기 때문입니다. 같은 패턴은 키/값 맵 객체에도 쓸 수 있습니다: const Status = { Active: "active", Banned: "banned" } as const로 선언한 뒤 type Status = (typeof Status)[keyof typeof Status].
const 타입 매개변수
제네릭 함수는 보통 넘겨받은 리터럴을 넓힙니다. TypeScript 5.0부터는 타입 매개변수에 const를 표시할 수 있습니다. 그러면 호출하는 쪽에 as const를 쓰라고 요구하지 않고도, 인수에 as const가 붙은 것처럼 컴파일러가 추론합니다.
이는 주로 라이브러리 작성자를 위한 도구입니다. 라우트 정의, 빌더, 스키마 헬퍼가 이를 써서 호출하는 쪽이 평범한 리터럴만으로도 정확한 타입을 얻게 합니다.
const의 여러 의미
const 키워드는 TypeScript 코드에서 서로 다른 네 곳에 나타납니다.
| 문법 | 종류 | 하는 일 |
|---|---|---|
const x = 1 | JavaScript 선언 | 바인딩을 다시 대입할 수 없음. 리터럴 값은 리터럴 타입을 유지 |
expr as const | TypeScript 단언 | 가장 좁은 타입: 리터럴, readonly 속성, readonly 튜플 |
function f<const T>() | TypeScript 타입 매개변수 | 인수에 as const가 붙은 것처럼 추론 |
const enum E {} | TypeScript enum | 멤버가 컴파일 시점에 인라인되는 enum |
어느 것도 런타임에 객체를 동결하지 않습니다. const obj = { a: 1 }이어도 obj.a = 2는 허용되고, obj 자체를 다시 대입하는 것만 오류입니다.
자주 묻는 질문
TypeScript에서 리터럴 타입이란 무엇인가요?
값을 정확히 하나만 허용하는 타입입니다. "GET"은 유일한 값이 문자열 "GET"인 타입이고, 404는 유일한 값이 숫자 404인 타입이며, true는 유일한 값이 true인 타입입니다. type Method = "GET" | "POST"처럼 유니언으로 묶을 때 가장 유용합니다.
TypeScript에서 as const는 무엇을 하나요?
as const는 const 단언(const assertion)입니다. 표현식에 대해 가장 좁은 타입을 추론하라고 컴파일러에 알립니다. 문자열과 숫자 값은 리터럴 타입을 유지하고, 객체 속성은 readonly가 되며, 배열 리터럴은 readonly 튜플이 됩니다. 타입만 바뀝니다. 런타임의 값은 같은 평범한 객체나 배열이고, 동결되지 않습니다.
TypeScript가 내 리터럴 대신 string으로 추론하는 이유는 무엇인가요?
값이 변경 가능하기 때문입니다. let x = "a"와 { method: "GET" }의 속성은 나중에 다시 대입될 수 있으므로 TypeScript는 string으로 넓힙니다. const 변수는 리터럴 타입 "a"를 유지합니다. 객체 안의 리터럴을 유지하려면 리터럴 타입으로 표기하거나, as const나 satisfies를 쓰세요.
const와 as const의 차이는 무엇인가요?
const는 JavaScript 선언입니다. 변수를 다시 대입할 수 없지만 가리키는 객체는 여전히 바꿀 수 있습니다. as const는 TypeScript 타입 단언입니다. 타입 시스템에서 값의 모든 속성을 readonly이자 리터럴로 만듭니다. 둘 다 런타임에 객체를 동결하지 않으며, 그러려면 Object.freeze를 쓰세요.
문자열 배열에서 유니언 타입을 얻으려면 어떻게 하나요?
배열을 as const로 선언한 다음 그 타입을 number로 인덱싱합니다. const roles = ["admin", "user"] as const; type Role = (typeof roles)[number];은 "admin" | "user"가 됩니다. as const가 없으면 배열은 string[]이고 결과는 그냥 string입니다.