← 전체 글로 돌아가기

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를 적용하는 게 차후 문제를 줄일 수 있다.