TypeScript: 'Type string is not assignable to type...' 리터럴 타입 할당 오류 해결법 (as const)

TypeScript Type string is not assignable to type 리터럴 타입 할당 오류 해결 가이드
3초 요약: 이렇게 고치세요
  • 원인: 일반 객체 리터럴을 선언하면 컴파일러가 속성 값을 넓은 string 타입으로 추론(Type Widening)해서, 좁은 리터럴 유니온 타입 파라미터에 넘기면 막힘
  • 해결: 객체 선언 끝에 as const 한 번만 붙여서 모든 하위 속성을 리터럴 상수로 고정
  • 주의: 필드마다 as 'admin'처럼 개별 단언을 붙이는 건 중복 코드가 늘고 구조 변경 시 놓치기 쉬움 — as const 하나로 끝내는 게 낫다
const config = { theme: 'dark', apiTimeout: 5000 } as const;
↓ 왜 이런 에러가 나는지 원리 보기

설정 객체의 키 값을 그대로 함수에 넘겼는데도 TypeScript가 'Type string is not assignable to type ...' 에러를 내는 경우가 있습니다. 원인은 객체 리터럴을 선언하면 컴파일러가 속성 값을 넓은 string 타입으로 추론하기 때문이며, as const 단언으로 해결할 수 있습니다. 원인과 적용 패턴을 정리합니다.


왜 텍스트가 똑같은데 'string'으로 판정되어 할당 에러가 날까

타입스크립트가 이 에러를 발생시키는 이유는 자바스크립트의 본질적인 '동적 객체 수정 가능성' 때문입니다. 기본적으로 자바스크립트에서 const user = { role: 'admin' }과 같이 객체를 정의하면, 선언 자체는 상수가 맞지만 객체 내부의 속성인 user.role 값은 런타임에 user.role = 'editor'처럼 얼마든지 다른 문자열로 재할당될 수 있습니다.

타입스크립트 컴파일러는 이러한 자바스크립트의 유연한 특성을 존중하기 위해, 객체 내부의 문자열 속성을 자동으로 가장 넓은 단위인 일반 string 타입으로 확장하여 추론하는데, 이를 타입 넓히기(Type Widening)라고 부릅니다. 반면, 우리가 함수나 변수 타입 명세에 지정해 둔 'admin' | 'manager' | 'user'와 같은 선언은 결합 리터럴(Literal) 타입으로, 오직 지정된 3가지의 특정한 문자열만 받아들이는 좁고 엄격한 안전 통로입니다. 따라서 컴파일러 입장에서는 "언제 어떤 값(예: 'guest' 등)으로 바뀔지 모르는 string 타입의 변수를, 어떻게 3가지만 허용하는 좁은 리터럴 통로에 안전하다고 판단해서 들여보내겠어?"라며 컴파일을 중단시키는 것입니다.

예를 들어 const config = { role: 'admin' }처럼 일반 객체로 선언하면 컴파일러는 role을 넓은 string 타입으로 추론하기 때문에, 엄격한 리터럴 타입 파라미터에 전달하려 하면 타입이 맞지 않아 에러가 납니다. 반면 const config = { role: 'admin' } as const처럼 끝에 as const를 붙이면 모든 속성 값이 읽기 전용 상수 'admin' 자체로 고정되어 타입 체크를 그대로 통과합니다.


실무에서 흔히 마주치는 as const 적용 패턴

중대형 TypeScript 프로젝트의 코드 리뷰에서 이와 관련된 실수는 꽤 자주 눈에 띕니다. 가장 좋지 않은 임시방편은 에러가 나는 곳마다 수동으로 role: 'admin' as 'admin' 혹은 role: userSetting.role as UserRole처럼 강제 타입 단언(Type Assertion)을 하나씩 붙이는 조치입니다.

이런 방식은 객체의 필드가 늘어날 때마다 중복 타이핑이 발생하고, 추후 타입 구조가 변경될 때 컴파일러의 자동 에러 감지 이점을 잃게 만드는 주요 원인이 됩니다. 개별 타입 단언을 전부 걷어내고 객체 선언부 맨 끝에 as const 단언문 딱 한 번만 지정해 하위 모든 뎁스(Depth)의 값을 읽기 전용 상수(Readonly Literal)로 일괄 고정하는 방식이 훨씬 안전합니다. 중복 코드를 줄이는 동시에 유지보수 안정성도 함께 올라갑니다.

설정 객체 하나에 필드별로 as 단언을 하나씩 붙여둔 코드를 리뷰하다가, 새 필드가 추가됐는데 단언을 안 붙여서 타입이 조용히 넓혀져 있던 걸 발견한 적이 있습니다. 컴파일은 통과했지만 실제로는 의도한 리터럴 타입이 아니었던 셈입니다. 그 뒤로는 객체 전체에 as const 하나만 붙이도록 리뷰 규칙을 바꿨습니다.


'as const' 단언문 적용 해결 코드 예시

리터럴 타입 불일치 에러를 방지하고 컴파일러 체크를 통과하는 작성 패턴입니다.

// 1. 특정 테마 모드만 허용하도록 정의된 엄격한 결합 리터럴 타입
type ThemeMode = 'light' | 'dark' | 'system';

interface AppConfig {
  theme: ThemeMode;
  apiTimeout: number;
}

function initializeApp(config: AppConfig) {
  console.log(`[설정] 테마 모드: ${config.theme}, 타임아웃: ${config.apiTimeout}ms`);
}

// 에러 유발 코드 (Type Widening 발생)
const badConfig = {
  theme: 'dark', // 컴파일러는 이 값을 변경 가능한 일반 string으로 추론함
  apiTimeout: 5000
};
// 컴파일 오류 발생: Argument of type '{ theme: string; apiTimeout: number; }' is not assignable...
initializeApp(badConfig);


// 해결 방법

// 해결 코드 (as const 적용으로 객체의 모든 멤버를 동결)
const goodConfig = {
  theme: 'dark',
  apiTimeout: 5000
} as const; // 모든 값이 리터럴 상수 및 readonly 속성으로 전환됨

// 컴파일러 에러 없이 통과
initializeApp(goodConfig);

해결 코드의 라인별 동작 해설

위 코드에서 첫 번째 badConfig 객체는 변수를 선언할 때 리터럴 타입인 ThemeMode 인터페이스를 명시하지 않았기 때문에, theme 필드의 타입은 자동으로 string으로 초기화됩니다. 이로 인해 initializeApp 함수에 전달될 때, 타입스크립트 컴파일러가 타입 충돌 예외 경고를 발생시킵니다. 반면, 해결 코드의 goodConfig 선언의 끝자리에 붙인 as const 지시어는 컴파일러에게 이 객체의 모든 프로퍼티 값을 변경이 불가능한 원시 리터럴 자체로 고정하라는 지시를 내립니다. as const 단언이 적용됨에 따라 goodConfig.theme의 최종 타입은 string이 아니라 오직 문자열 리터럴 'dark' 타입으로 좁혀지게 됩니다. 덕분에 ThemeMode의 구조 규칙과 부합하여 컴파일 체킹 단계를 그대로 통과하게 되는 원리입니다.


요약: 일반 객체 선언 vs as const 속성 차이

타입을 고정할 때 취할 수 있는 세부 제어 방식의 차이를 정리했습니다.

제어 방식 타입 추론 및 제약 방식 가독성 및 추천 상황
일반 타입 선언 (: string) 속성의 값이 언제든지 변경될 수 있는 일반 문자열 변수로 지정됨. 동적 할당 변수 권장
개별 강제 단언 (as 'admin') 특정 키 값만 강제로 상수로 덮어 씌우는 수동 조치 (하위 depth 자동 동결 불가). 비추천 (실수 리스크)
상수 전체 단언 (as const) 객체 내부의 모든 하위 계층 속성들이 완전히 '읽기 전용 리터럴' 타입으로 일괄 동결됨. 환경 설정, API 공통 상수에 권장
추가 추천 자료 및 공식 레퍼런스

타입스크립트의 Const Assertions 문법과 리터럴 추론에 대한 상세 명세는 공식 문서에서 확인할 수 있습니다.

호마다의 웹개발 팁

필드가 여러 개인 설정 객체라면 속성마다 개별적으로 타입 단언을 붙이기보다, 객체 선언 끝에 as const 하나만 붙이는 편이 유지보수하기 더 편합니다.

댓글

이 블로그의 인기 게시물

TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)

Next.js 하이드레이션 오류 원인과 해결법 완벽 정리

Next.js App Router에서 getServerSideProps 대체 및 데이터 페칭법