빠른 비교
function defineRoutes<const T extends readonly string[]>(routes: T) {
return routes;
}
const routes = defineRoutes(["/", "/settings", "/billing"]);
// readonly ["/", "/settings", "/billing"]| 방식 | 리터럴 보존 책임 |
|---|---|
as const | 호출자가 값 옆에 직접 붙임 |
const T | API 작성자가 제네릭 추론 정책으로 지정 |
일반 T | 보통 더 넓은 타입으로 추론 |
추론 방식
const type parameter는 제네릭 인자 추론을 더 좁게 잡는다
일반 제네릭은 객체와 배열 리터럴을 받을 때 나중의 변경 가능성을 고려해 넓은 타입으로 추론하는 일이 많습니다. const modifier를 type parameter에 붙이면 호출자가 매번 as const를 붙이지 않아도 리터럴 형태를 더 잘 보존합니다.
function tuple<T extends readonly string[]>(value: T): T {
return value;
}
function constTuple<const T extends readonly string[]>(value: T): T {
return value;
}
const a = tuple(["read", "write"]);
// string[]
const b = constTuple(["read", "write"]);
// readonly ["read", "write"]이 기능은 값 자체를 런타임에서 고정하지 않습니다. const T는 타입 추론 정책이고, 런타임 불변성은 Object.freeze, readonly 데이터 구조, 복사 정책 같은 별도 선택입니다.
API 작성자 쪽에서 as const 요구를 줄인다
라이브러리형 함수에서 호출자에게 as const를 요구하면 사용성이 떨어집니다. 라우트, 액션 타입, 컬럼 정의, 이벤트 이름처럼 리터럴 목록이 API 계약이 되는 함수는 const T가 잘 맞습니다.
function createActions<
const T extends Record<string, (...args: any[]) => unknown>
>(actions: T) {
return actions;
}
const actions = createActions({
login: (id: string) => ({ type: "login", id }),
logout: () => ({ type: "logout" }),
});
type ActionName = keyof typeof actions;
// "login" | "logout"반대로 내부에서 값을 자유롭게 변경하거나, 호출자에게 넓은 string[], Record<string, T> 타입을 기대하는 API라면 const T가 오히려 타입을 과하게 구체화할 수 있습니다.
constraint는 추론 가능한 모양을 제한한다
const T만 붙인다고 아무 값이나 좋은 타입으로 잡히는 것은 아닙니다. T extends readonly string[]처럼 constraint를 함께 설계해야 함수 안에서 필요한 연산을 안전하게 할 수 있습니다.
function first<const T extends readonly unknown[]>(items: T): T[0] {
return items[0];
}
const value = first(["primary", 1, true]);
// "primary"constraint가 너무 넓으면 함수 안에서 쓸 수 있는 정보가 줄고, 너무 좁으면 호출 가능한 값이 줄어듭니다. const T는 추론을 보존하는 장치이고, 유효한 입력 범위는 constraint가 정합니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 호출자 리터럴 목록이 API 계약임 | const T |
| 호출자가 한 번만 고정하면 충분함 | as const |
| 배열을 내부에서 변경해야 함 | 일반 T 또는 mutable 배열 타입 |
| 라이브러리 helper가 값에서 타입을 뽑음 | const T extends ... |
| 값의 런타임 불변성이 필요함 | 타입이 아니라 런타임 고정 방식 검토 |
const T는 "더 정확한 추론"이 목표일 때 씁니다. 단순히 모든 제네릭에 붙이면 readonly tuple과 리터럴 타입이 과하게 전파되어 구현 내부에서 다시 타입을 풀어야 하는 상황이 생길 수 있습니다.
주의할 점
const type parameter는 컴파일 타임 추론 기능입니다. 값을 실제로 얼리거나 복사를 막지 않으므로 보안, 동시성, 상태 변경 방지 용도로 오해하면 안 됩니다.
function useConfig<const T extends { mode: string }>(config: T) {
config.mode = "prod";
}위처럼 constraint가 mutable이면 함수 내부에서 변경이 가능합니다. 리터럴 타입 보존과 mutation 금지는 별개의 문제이므로, 필요한 경우 readonly constraint를 함께 써야 합니다.
참고 링크
2 sources