as const는 값을 readonly 리터럴 타입으로 좁혀, 배열이나 객체에 들어 있는 실제 값을 기준으로 union 타입을 만들 수 있게 해줍니다. 이 글에서는 typeof, 인덱스 접근 타입, keyof를 조합해 상수 데이터에서 타입을 추출하는 실무 패턴을 정리합니다.as const가 필요한 이유

TypeScript에서 문자열 목록을 다루다 보면 값과 타입을 따로 관리하는 코드가 자주 생깁니다. 예를 들어 주문 상태가 pending, paid, shipped 세 가지라면 화면 렌더링용 배열도 필요하고, 함수 파라미터를 제한하기 위한 union 타입도 필요합니다.
const statuses = ["pending", "paid", "shipped"];
type Status = "pending" | "paid" | "shipped";
function updateStatus(status: Status) {
// ...
}
처음에는 이 방식도 문제가 없어 보입니다. 하지만 상태값이 추가되거나 이름이 바뀌면 배열과 타입을 함께 수정해야 합니다. 한쪽만 고치면 런타임 값과 타입 선언이 어긋납니다. 화면에는 cancelled가 옵션으로 보이는데 타입에서는 허용되지 않거나, 반대로 타입에는 있는데 실제 목록에는 없는 값이 생길 수 있습니다.
as const 기반 패턴의 핵심은 값 목록을 타입의 기준점으로 삼는 것입니다. 사람이 union 타입을 다시 쓰는 대신, 이미 존재하는 상수 데이터에서 타입을 뽑아냅니다. 이렇게 하면 상태값, 권한 코드, 라우트 이름, 탭 이름, API 응답 코드처럼 반복해서 쓰이는 문자열 목록을 한 곳에서 관리할 수 있습니다.
as const의 기본 동작
const 선언과 as const는 비슷해 보이지만 타입 추론 결과가 다릅니다. const는 변수 재할당을 막지만, 배열이나 객체 내부 값의 타입까지 항상 가장 좁게 고정하지는 않습니다.
const status = "pending";
// type: "pending"
const statuses = ["pending", "paid", "shipped"];
// type: string[]
단일 문자열을 const로 선언하면 TypeScript는 그 값을 string이 아니라 "pending" 리터럴 타입으로 추론합니다. 하지만 배열은 다릅니다. 일반 배열은 나중에 다른 문자열을 추가할 수 있으므로 string[]으로 넓게 추론됩니다.
const statuses = ["pending", "paid", "shipped"] as const;
// type: readonly ["pending", "paid", "shipped"]
as const를 붙이면 배열은 readonly tuple로 추론됩니다. 각 요소는 넓은 string이 아니라 정확한 리터럴 타입이 됩니다. 객체에서도 마찬가지입니다. 프로퍼티 값이 가능한 한 좁은 리터럴 타입으로 추론되고, 프로퍼티는 readonly가 됩니다.
const STATUS_LABEL = {
pending: "결제 대기",
paid: "결제 완료",
shipped: "배송 중",
} as const;
// type:
// {
// readonly pending: "결제 대기";
// readonly paid: "결제 완료";
// readonly shipped: "배송 중";
// }
여기서 readonly는 런타임 객체를 깊게 얼린다는 뜻이 아닙니다. TypeScript 타입 시스템 안에서 이 값을 변경 가능한 일반 객체로 보지 않겠다는 의미에 가깝습니다. 런타임 불변성이 필요하다면 별도의 설계가 필요하지만, 타입 추출 목적이라면 as const만으로 충분한 경우가 많습니다.
배열에서 union 타입 자동 생성하기
배열에서 union 타입을 만드는 대표 패턴은 typeof 배열[number]입니다. 처음 보면 낯설지만 의미는 단순합니다. 먼저 typeof로 값의 타입을 가져오고, [number]로 배열 요소 타입을 꺼냅니다.
const statuses = ["pending", "paid", "shipped"] as const;
type Status = typeof statuses[number];
// "pending" | "paid" | "shipped"
typeof statuses는 readonly ["pending", "paid", "shipped"]입니다. 여기에 [number]를 붙이면 숫자 인덱스로 접근 가능한 모든 요소의 타입을 합친 결과가 됩니다. 그래서 최종 타입은 "pending" | "paid" | "shipped"가 됩니다.
이 패턴은 옵션 목록을 화면과 타입에서 동시에 써야 할 때 특히 유용합니다. 예를 들어 필터 버튼을 렌더링하면서 선택 가능한 값도 제한하려면 같은 배열을 기준으로 삼을 수 있습니다.
const orderStatuses = ["pending", "paid", "shipped", "cancelled"] as const;
type OrderStatus = typeof orderStatuses[number];
function canRefund(status: OrderStatus) {
return status === "paid" || status === "shipped";
}
orderStatuses.map((status) => ({
value: status,
refundable: canRefund(status),
}));
이제 상태값을 추가할 때는 orderStatuses 배열만 수정하면 됩니다. union 타입은 자동으로 따라옵니다. 코드 리뷰에서도 확인할 지점이 줄어듭니다. 타입 선언과 실제 값 목록 중 어느 쪽이 진짜인지 고민할 필요 없이, 상수 배열이 기준이 됩니다.
객체에서 union 타입 자동 생성하기

객체에서는 key를 union으로 만들 수도 있고 value를 union으로 만들 수도 있습니다. 두 패턴은 비슷하지만 쓰임새가 다릅니다. key가 코드 내부 식별자라면 keyof typeof를 사용하고, value가 외부 시스템과 주고받는 실제 값이라면 value union을 사용합니다.
const ROLE = {
admin: "ADMIN",
editor: "EDITOR",
viewer: "VIEWER",
} as const;
type RoleKey = keyof typeof ROLE;
// "admin" | "editor" | "viewer"
type RoleCode = typeof ROLE[keyof typeof ROLE];
// "ADMIN" | "EDITOR" | "VIEWER"
keyof typeof ROLE는 객체의 key 목록을 union으로 만듭니다. 반면 typeof ROLE[keyof typeof ROLE]는 모든 key로 접근했을 때 나올 수 있는 value를 union으로 만듭니다. 즉 key union은 객체의 속성 이름 기준이고, value union은 실제 값 기준입니다.
권한 코드처럼 서버나 데이터베이스에 저장되는 값이 ADMIN, EDITOR, VIEWER라면 함수 파라미터는 value union을 받는 편이 자연스럽습니다.
function hasAccess(role: RoleCode) {
return role === ROLE.admin || role === ROLE.editor;
}
hasAccess("ADMIN"); // 가능
hasAccess("admin"); // 오류
반대로 화면 코드에서 ROLE.admin처럼 객체 key를 기준으로 분기하거나 설정 객체를 만들고 싶다면 key union이 더 적합할 수 있습니다.
const roleLabels: Record<RoleKey, string> = {
admin: "관리자",
editor: "편집자",
viewer: "조회자",
};
key union과 value union 중 무엇을 기준으로 삼을까
실무에서는 key와 value 중 무엇을 타입의 기준으로 삼을지 먼저 정해야 합니다. 기준은 문법 취향이 아니라 데이터가 어디에서 쓰이는지에 따라 달라집니다.
- 코드 내부에서 읽기 쉬운 식별자가 중요하면 key union을 사용합니다.
- 서버, URL, 데이터베이스, 외부 API와 주고받는 값이 중요하면 value union을 사용합니다.
- 배열 렌더링과 선택값 제한이 목적이면 상수 배열에서 union을 추출하는 방식이 단순합니다.
- 라벨, 정렬 순서, 색상 같은 부가 정보가 함께 필요하면 객체나 객체 배열을 기준 데이터로 삼는 편이 좋습니다.
예를 들어 라우트 이름은 코드 내부에서 식별자로 자주 쓰이므로 key union이 어울릴 수 있습니다. 하지만 결제 상태 코드처럼 API 응답에 포함되는 값은 value union을 기준으로 삼는 편이 안전합니다.
const PAYMENT_STATUS = {
waiting: "WAITING",
approved: "APPROVED",
failed: "FAILED",
} as const;
type PaymentStatus = typeof PAYMENT_STATUS[keyof typeof PAYMENT_STATUS];
function parsePaymentStatus(status: PaymentStatus) {
switch (status) {
case PAYMENT_STATUS.waiting:
return "결제 대기";
case PAYMENT_STATUS.approved:
return "승인 완료";
case PAYMENT_STATUS.failed:
return "결제 실패";
}
}
이 코드에서는 외부에서 들어오는 값이 WAITING, APPROVED, FAILED이므로 value union이 맞습니다. 내부 key인 waiting, approved, failed는 사람이 읽기 쉬운 이름으로 남겨두고, 실제 데이터 계약은 value로 표현합니다.
enum, 수동 union, as const 비교
as const 패턴이 항상 enum을 대체해야 하는 것은 아닙니다. enum은 런타임 객체가 필요하거나 기존 코드베이스에서 이미 일관되게 쓰고 있다면 여전히 선택할 수 있습니다. 다만 단순한 문자열 목록을 기준으로 타입을 만들고 싶다면 as const가 더 가볍고 JavaScript 값과 잘 맞는 경우가 많습니다.
수동 union 타입은 가장 직관적입니다. 하지만 값 목록이 따로 있을 때 중복이 생깁니다. enum은 이름 있는 상수 집합을 만들 수 있지만, 빌드 결과와 사용 방식에 대한 이해가 필요합니다. as const는 이미 존재하는 배열이나 객체를 그대로 타입의 출처로 사용할 수 있다는 점이 장점입니다.
// 값 목록과 타입이 분리된 예
const tabs = ["home", "profile", "settings"];
type Tab = "home" | "profile";
// settings가 빠져 있음
이런 불일치는 작은 코드에서는 금방 찾을 수 있지만, 옵션이 여러 파일에서 재사용되면 놓치기 쉽습니다. as const를 사용하면 타입이 값에서 파생되므로 이런 종류의 중복을 줄일 수 있습니다.
const tabs = ["home", "profile", "settings"] as const;
type Tab = typeof tabs[number];
중요한 점은 as const가 타입을 더 멋지게 보이게 하는 문법이 아니라는 것입니다. 기준 데이터를 어디에 둘지 정하고, 그 기준에서 타입을 자동으로 뽑아내는 방식입니다. 따라서 기준 데이터가 불분명하거나 값이 너무 자주 동적으로 바뀌는 경우에는 억지로 적용하지 않는 편이 낫습니다.
실무 적용 기준
as const는 다음과 같은 상황에서 특히 잘 맞습니다. 문자열 또는 숫자 값의 후보가 제한되어 있고, 그 후보 목록을 런타임 코드에서도 실제로 사용할 때입니다. 예를 들어 셀렉트 박스 옵션, 탭 목록, 정렬 기준, 권한 코드, 알림 타입, API 상태값 등이 여기에 해당합니다.
- 상수 배열은 순서가 중요하고 목록 렌더링이 필요한 경우에 적합합니다.
- 상수 객체는 코드에서 의미 있는 이름과 실제 값을 분리하고 싶을 때 적합합니다.
- 객체 배열은 value, label, color, icon 같은 부가 정보를 함께 관리할 때 적합합니다.
- 외부 입력 검증까지 필요하다면 타입만 믿지 말고 런타임 검증 로직도 함께 둬야 합니다.
const sortOptions = [
{ value: "latest", label: "최신순" },
{ value: "popular", label: "인기순" },
{ value: "price_low", label: "낮은 가격순" },
] as const;
type SortValue = typeof sortOptions[number]["value"];
// "latest" | "popular" | "price_low"
객체 배열에서도 같은 원리가 적용됩니다. typeof sortOptions[number]는 배열 요소 객체의 union 타입이고, 거기서 ["value"]에 접근하면 각 객체의 value 값만 모은 union 타입이 됩니다. UI 옵션 목록과 선택 가능한 값 타입을 같은 데이터에서 관리할 수 있습니다.
다만 모든 객체에 as const를 붙이는 습관은 피하는 편이 좋습니다. 값이 이후에 변경되어야 하는 상태 객체, 사용자 입력을 담는 객체, 테스트에서 일부 필드를 바꿔야 하는 fixture에는 readonly 추론이 오히려 번거로울 수 있습니다. 후보 값이 고정된 상수 데이터인지 먼저 확인하는 것이 좋습니다.
최근 TypeScript 문법과 함께 보면 역할도 더 분명해집니다. satisfies는 값이 특정 구조를 만족하는지 검사하면서 추론을 보존할 때 유용하고, const type parameters는 제네릭 함수 안에서 리터럴 타입을 더 잘 유지하도록 돕습니다. 반면 이 글의 as const 패턴은 이미 선언한 상수 배열이나 객체를 readonly 리터럴 데이터로 고정한 뒤, 그 값에서 union 타입을 뽑아내는 데 초점이 있습니다.
마무리
as const의 핵심은 값의 타입을 넓히지 않고 정확한 리터럴 타입으로 유지하는 것입니다. 배열에서는 typeof 배열[number]로 요소 union을 만들고, 객체에서는 keyof typeof 객체로 key union을, typeof 객체[keyof typeof 객체]로 value union을 만들 수 있습니다.
이 패턴을 사용하면 값 목록과 타입 선언을 따로 관리하면서 생기는 불일치를 줄일 수 있습니다. 상태값, 옵션 목록, 권한 코드, 라우트 이름처럼 값이 먼저 있고 타입이 그 값을 따라가야 하는 상황이라면 as const, typeof, keyof 조합은 유지보수 비용을 줄이는 현실적인 선택이 됩니다.