TypeScript 'Type is not assignable' 에러 원인 및 해결법

TypeScript 타입 불일치 에러 원인 및 타입 좁히기 해결 가이드
3초 요약: 이렇게 고치세요
  • 원인: 넓은 타입(string)을 좁은 타입(유니온 리터럴)에 그대로 대입하려 하거나, 정의에 없는 잉여 프로퍼티가 든 객체 리터럴을 직접 대입해서 발생
  • 해결: as로 좁혀서 단언 / 객체는 중간 변수에 담았다가 대입 / 유니온 타입은 typeof·in으로 분기해서 좁히기
  • 주의: as 단언은 컴파일러 검사를 우회하는 것이라 실제로 그 타입이 맞는지는 개발자가 책임져야 함
userStatus = inputStatus as UserStatus; // string -> 리터럴 유니온
↓ 왜 이런 에러가 나는지 원리 보기

타입스크립트 코딩을 할 때 가장 자주 마주치는 경고가 "Type 'A' is not assignable to type 'B'"(A 타입을 B 타입에 할당할 수 없습니다)일 겁니다. 언뜻 데이터 구조가 똑같아 보이는데 왜 불일치 경고가 나는지, 실전에서 타입을 좁혀 해결하는 방법을 정리했습니다.


왜 'not assignable' 에러가 발생하는가

이 에러의 본질은 "타입 호환성(Type Compatibility)의 규칙 위반"에 있습니다. 타입스크립트는 정적 컴파일 단계에서 값이 안전하게 대입될 수 있는지 검사합니다. 상위 개념의 넓은 타입에 하위 개념의 구체적인 타입을 넣는 것은 안전하지만(예: string"admin" 리터럴 대입), 반대로 더 넓은 범주의 타입을 구체적인 타입 칸에 강제로 집어넣으려고 할 때 컴퓨터는 런타임 크래시를 우려해 할당 거부 경고를 내보냅니다. API 응답 값을 그대로 유니온 타입 변수에 넣으려다가 이 에러를 처음 마주치는 경우가 많습니다.

타입 할당 가능 범위 시각화

허용되는 방향 (Upcast) 구체적인 타입 -> 넓은 타입
(예: "admin" 리터럴 타입을 string 타입 변수에 대입)
차단되는 방향 (Downcast) 넓은 타입 -> 구체적인 타입
(예: 일반 string 타입을 "admin" 전용 변수에 대입 시 에러)

대표적인 2가지 오류 상황과 해결 기법

1. 유니온 타입(Union Type)과 리터럴 할당 에러

특정 지정 문자열 목록만을 허용하는 유니온 타입에 일반 string 변수를 대입하려 할 때 발생합니다.

[잘못된 예시]

type UserStatus = "active" | "suspended" | "inactive";

let inputStatus: string = "active"; // 일반 string 타입
let userStatus: UserStatus;

// 에러: Type 'string' is not assignable to type 'UserStatus'.
userStatus = inputStatus;

[올바른 해결 코드]

type UserStatus = "active" | "suspended" | "inactive";

let inputStatus: string = "active";
let userStatus: UserStatus;

// 해결책 A: 'as' 키워드로 타입을 좁혀 할당하기
userStatus = inputStatus as UserStatus;

// 해결책 B: 애초에 변수 타입을 리터럴로 선언하기
const fixedStatus: UserStatus = "active";
userStatus = fixedStatus;

2. 객체 매핑 시 '초과 프로퍼티 검사' 우회

객체 리터럴을 변수에 직접 할당할 때, 정의된 타입 사양에 없는 잉여 프로퍼티가 있으면 오류가 터집니다.

[잘못된 예시]

interface Point {
  x: number;
  y: number;
}

// 에러: Object literal may only specify known properties, and 'z' does not exist...
const myPoint: Point = { x: 10, y: 20, z: 30 };

[올바른 해결 코드]

interface Point {
  x: number;
  y: number;
}

const rawData = { x: 10, y: 20, z: 30 };

// 중간 변수(rawData)를 통해 대입하면 구조적 타이핑에 의해 안전하게 할당됩니다.
const myPoint: Point = rawData;

타입스크립트 실전 타입 좁히기(Narrowing) 기법

방법 A: typeof를 사용한 원시 타입 분기

function printLength(input: string | number) {
  if (typeof input === "string") {
    // string 타입으로 자동 좁히기
    console.log(input.length);
  } else {
    // number 타입으로 자동 좁히기
    console.log(input.toFixed(2));
  }
}

방법 B: in 연산자를 활용한 커스텀 객체 식별

interface Admin { privileges: string[]; }
interface Employee { startDate: Date; }

function checkUser(user: Admin | Employee) {
  if ("privileges" in user) {
    // Admin 타입으로 타입 자동 좁히기 완료
    console.log(user.privileges);
  } else {
    // Employee 타입으로 타입 자동 좁히기 완료
    console.log(user.startDate);
  }
}

결제 수단이 카드/계좌이체로 나뉘는 화면을 만들 때, 두 타입을 유니온으로 묶어두고 payment.cardNumber처럼 한쪽에만 있는 필드에 바로 접근하려다 컴파일 에러를 여러 번 받은 적이 있습니다. 처음엔 그냥 as로 단언해서 넘어갔는데, 나중에 실제로 계좌이체 데이터가 들어왔을 때 cardNumberundefined로 조용히 새는 버그가 났습니다. in 연산자로 분기하도록 바꾼 뒤로는 그런 종류의 실수가 컴파일 단계에서 걸러졌습니다.


요약: 타입 불일치 에러 대응 체크리스트

넓은 범주의 값을 좁은 타입 칸에 대입하려 할 때는 as 키워드로 구체적인 타입임을 단언해 주는 것이 기본입니다. 객체 리터럴을 직접 대입하는데 정의되지 않은 초과 필드가 있다면, 중간 객체 변수에 한 번 담았다가 간접적으로 대입하는 우회 방식이 잉여 프로퍼티 검사를 피하는 방법입니다. 유니온 타입의 속성을 참조할 때는 typeofin 연산자로 타입을 분기해 좁혀두면 공통되지 않은 프로퍼티 접근으로 인한 오류를 막을 수 있습니다.

공식 참고 레퍼런스

타입 호환성과 좁히기 원리는 공식 문서에서 더 자세히 확인할 수 있습니다.

호마다의 웹개발 팁

타입 호환성도 결국 자바스크립트의 기본 자료형들이 서로 어떻게 다른지 아는 데서 출발합니다. 기초가 헷갈리신다면 [호마다의 IT 개발 입문 블로그] 의 자료형 기초 글도 참고해보세요.

댓글

이 블로그의 인기 게시물

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

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

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