2026. 7. 17.

TypeScript Property does not exist on type never, 빈 배열과 분기부터 구분하세요

TypeScript가 있는 속성을 never라고 판단할 때 빈 컬렉션, 소진된 분기, 제네릭 문맥 누락을 구분하고 최소 타입 수정 뒤 다시 검사하는 순서다.

3 min read
TypeScript Property does not exist on type never, 빈 배열과 분기부터 구분하세요 대표 이미지

객체에 name 속성이 분명히 있는데 편집기가 Property 'name' does not exist on type 'never'라고 막으면, 속성을 추가할 때가 아니다. 오류 위치에서 hover로 추론 타입을 보고 그 값이 만들어진 선언부로 이동해야 한다. never는 “속성이 없는 객체”가 아니라 TypeScript가 가능한 값을 하나도 남기지 못한 상태다.

TypeScript never 오류에서 hover 타입과 선언부를 함께 확인하는 개발 작업 장면

원인은 대개 세 갈래다. 초기값만으로 원소 타입을 알 수 없는 컬렉션, 조건문이 가능한 타입을 모두 제거한 분기, 입력과 출력의 관계가 끊긴 제네릭이다. 오류 줄에 as any를 붙이면 이 차이가 가려진다. 먼저 never[]인지, 단일 never인지, 제네릭 결과가 never인지를 구분한다.

오류 줄보다 값이 만들어진 선언부를 먼저 본다

편집기에서 오류가 난 값에 마우스를 올려 타입을 확인한다. itemsnever[]인지, selectednever인지, 함수 반환값이 never로 굳었는지 적는다. 그런 다음 “이 값은 어디에서 처음 만들어졌는가”를 찾는다. 오류가 난 속성 접근은 결과일 뿐이고 선언·초기화·분기 가운데 한 곳에서 타입 문맥이 끊긴 경우가 많다.

다음 네 줄만 채우면 진단 범위가 작아진다.

오류 문장: Property 'name' does not exist on type 'never'.
오류 위치의 추론 타입: never
값이 만들어진 선언부: const selected = ...
직전 분기 또는 제네릭: if (...) / find(...) / useState(...)

TypeScript 공식 Narrowing 문서if, switch, 타입 가드와 대입을 따라 가능한 타입을 좁힌다고 설명한다. 모든 가능성이 제거되면 never가 남는다. 그래서 단일 속성을 억지로 열기보다 어떤 후보가 어디서 사라졌는지를 보는 편이 맞다. 빨간 줄이 여러 개라면 첫 TypeScript 오류 하나부터 고정하는 순서를 먼저 적용해도 된다.

빈 컬렉션은 원소 타입을 선언부에서 복구한다

빈 배열은 안에 든 값이 없어서 원소 타입을 보여 주지 못한다. 주변 문맥이나 컴파일러 설정에 따라 추론 결과는 달라질 수 있으므로 “빈 배열은 언제나 never[]다”라고 외우면 안 된다. 대신 상태·객체 프로퍼티·제네릭 인자처럼 나중에 들어올 값의 모양을 알고 있는 경계에서 타입을 선언한다.

interface User {
  id: string;
  name: string;
}

const users: User[] = [];
users.push({ id: "u1", name: "민수" });
console.log(users[0]?.name);

React 상태라면 초기값을 단언하기보다 제네릭에 의도를 둔다.

const [users, setUsers] = useState<User[]>([]);

공식 Type Inference 문서는 초기값과 주변 문맥으로 타입을 결정하고, 공통 타입을 고르기 어려운 곳에서는 명시 타입을 제공할 수 있다고 설명한다. 배열을 as User[]로 바꾸는 것보다 useState<User[]>나 변수 선언의 User[]가 낫다. 타입 정보가 값이 만들어지는 경계에 남기 때문이다.

TypeScript never 오류를 빈 배열, 분기 소진, 제네릭 문맥으로 나누는 진단 흐름

분기에서 never가 됐다면 조건이 후보를 모두 지웠는지 본다

선언부에 이미 User | null처럼 충분한 타입이 있다면 제어 흐름을 본다. 앞선 ifnullUser를 모두 처리한 뒤 남은 else에서 다시 name에 접근하면, 그 자리는 실제로 도달할 수 없는 상태다. 이때 속성을 강제로 열면 안 된다. 중복 분기를 지우거나 조건 순서를 바로잡는다.

function printName(user: User | null) {
  if (user === null) return;
  console.log(user.name);
}

switchdefault에서 never가 보이는 것은 정상일 수도 있다. 판별 가능한 유니온의 모든 경우를 처리했는지 검사하려고 의도적으로 never를 쓰기 때문이다. 반대로 평범한 렌더링 분기에서 값이 never가 됐다면, 앞 조건이 너무 넓거나 사용자 정의 타입 가드가 실제보다 강하게 선언됐는지 확인한다.

이 구분은 never를 없애는 작업과 다르다. 도달 불가능한 분기는 삭제하고, 실제로 도달해야 하는 분기는 타입 가드와 선언을 고친다. !나 이중 단언으로 통과시키면 런타임 값은 그대로라 다음 오류가 늦게 드러난다.

제네릭 문맥을 복구한 뒤 같은 검사 명령을 다시 실행한다

배열과 분기가 맞는데 find, 유틸 함수, 폼 훅, API 래퍼의 결과가 never라면 입력과 출력 사이 제네릭을 확인한다. TypeScript 공식 Generics 문서가 설명하듯 타입 매개변수는 입력의 타입 정보를 출력까지 이어 준다. 호출부나 함수 선언에서 이 관계가 빠지면 멤버를 안전하게 증명할 수 없다.

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const firstUser = first<User>(users);
console.log(firstUser?.name);

수정은 한 번에 하나만 적용한다. 그리고 프로젝트에 typecheck 스크립트가 있으면 그 명령을 실행한다. 없다면 프로젝트 루트에서 다음 명령으로 출력 파일 없이 타입 검사를 한다.

npx tsc --noEmit
TypeScript 오류 문장, 추론 타입, 최소 수정, 재실행 결과를 적는 검증 확인표

수정 전후에는 오류 문장, hover 타입, 바꾼 선언 한 곳, 재실행 결과를 남긴다. 오류가 사라졌다면 끝이다. 다른 오류가 나타나면 같은 순서로 새 첫 오류를 잡는다. 비슷하게 선택적 프로퍼티 때문에 막혔다면 possibly undefined를 좁혀서 고치는 기준을 이어서 확인할 수 있다.

as any는 마지막 해결책이 아니다. 공식 Everyday Types 문서에 따르면 any는 이후 타입 검사를 사실상 끄고, 타입 단언은 런타임 검사를 만들지 않는다. 외부 API 값처럼 컴파일러가 알 수 없는 데이터라면 단언보다 실제 값 검사와 타입 가드를 둔다. 내부 모델이 틀렸다면 선언부를 고친다. 이 두 경우를 구분해야 빨간 줄을 없앤 뒤에도 코드가 맞다.

참고 출처

이 글은 AI 코딩과 개발 학습의 일반 정보 제공 목적입니다. 도구, 모델, 커리큘럼, 요금은 버전과 시점에 따라 달라질 수 있으므로 실습이나 도입 전 공식 문서와 최신 릴리스 노트를 확인하세요.

다음으로 읽을 기사

같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.

댓글 0

이 글을 읽은 독자들의 생각을 나눠보세요.

비밀번호(선택)

첫 번째 댓글을 남겨보세요

여러분의 생각이 다른 독자에게 도움이 됩니다.