TypeScript
TypeScript as const 타입을 쓸 때 주의할 점
TypeScript의 as const 표현식을 리팩터링할 때 자주 마주치는 문제와 체계적인 디버깅 순서를 정리했다.
TypeScript의 as const는 깔끔해 보이지만, 리팩터링 과정에서 예상 밖의 타입 에러를 만날 수 있다. 특히 기존 코드에서 느슨한 타입으로 쓰던 부분을 좁혀낼 때 그렇다.
먼저 타입 에러가 발생한 위치를 확인한다
빌드 에러를 보면 보통 여러 곳에서 동시에 터진다. 하지만 진짜 원인은 대부분 한두 곳에 몰려 있다. 에러 메시지를 위에서 아래로 읽되, 첫 번째 에러 전후 코드부터 살펴보자.
// 기존: 느슨한 타입
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
};
// 변경: as const로 리터럴 타입 고정
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
} as const;
다른 곳에서 이 값을 수정하려는 코드가 있는지 확인
as const를 붙이면 타입이 readonly로 바뀌다 보니, 나중에 값을 할당하려는 코드에서 충돌한다. 예를 들어:
config.apiUrl = "https://new-api.example.com"; // 에러: readonly이므로 할당 불가
이 경우 할당하려는 코드를 지우거나, 변수를 as const 없이 선언해야 한다.
함수 인자로 전달할 때 타입 충돌 확인
as const로 만든 값을 함수에 넘길 때, 함수가 기대하는 타입이 너무 넓으면 불일치가 생긴다:
function setConfig(url: string, timeout: number) { }
// 이건 작동함
setConfig(config.apiUrl, config.timeout);
// 하지만 객체 전체를 넘기면?
setConfig(config); // 타입 에러: { readonly apiUrl: string; readonly timeout: number } != (string, number)
객체를 분해(destructure)해서 쓰는지 확인
함수 매개변수에서 직접 객체를 분해할 때도 as const의 영향을 받는다:
function handle({ apiUrl, timeout }: typeof config) { }
// apiUrl과 timeout의 타입이 이제 리터럴 타입이므로,
// 다른 곳에서 일반 string/number를 넘기면 타입 에러
배열이 포함된 경우 특히 주의
배열에 as const를 쓰면 각 요소가 고정되고, 배열 자체도 readonly가 된다:
const roles = ["admin", "user", "guest"] as const;
// roles의 타입: readonly ["admin", "user", "guest"]
// 배열에 요소를 추가하려고 하면?
roles.push("moderator"); // 에러: readonly 배열에는 push 불가
// 배열을 순회하면서 요소를 수정하려고 하면?
const newRoles = roles.map(r => r.toUpperCase());
// newRoles는 readonly string[]이 되는데,
// 일반 string[]를 기대하는 곳에 넘기면 타입 에러
타입 안전성과 유연성의 균형
문제를 정확히 파악하려면, 변경 전후의 타입 정의를 콘솔에 출력해보는 것도 방법이다. TypeScript 4.9 이상이면 satisfies 연산자를 써서 이런 충돌을 미리 감지할 수 있다:
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
} satisfies Record<string, string | number>;
// 이렇게 하면 as const 없이도 타입을 검사할 수 있다
리팩터링할 때 한 번에 큰 범위를 건드리기보다는, 먼저 현재 타입이 어떻게 쓰이는지 파악한 다음에 as const를 적용하는 게 차후 문제를 줄일 수 있다.