Menu

TypeScript never 타입: 완전성 검사와 never vs void

never는 값이 하나도 없는 타입입니다. 끝나지 않는 함수의 반환 타입이고, 좁히기가 모든 경우를 배제한 뒤 남는 타입이며, switch 완전성 검사를 가능하게 하는 도구입니다. never가 어디서 생기는지, void와 어떻게 다른지 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

never는 값이 하나도 없는 타입입니다. 반환 타입이 never인 함수는 정상적으로 반환하지 않습니다. 예외를 던지거나 영원히 실행됩니다. never 타입의 변수는 실행될 수 없는 코드를 표시하며, 그 덕분에 완전성 검사가 가능합니다.

never는 모든 타입에 대입할 수 있으므로 number를 반환하는 함수에서 return fail(...)이 컴파일됩니다. never 타입의 값은 실제로 도착할 수가 없기 때문입니다.

반환하지 않는 함수

끝에 도달하지 않는 함수는 두 종류입니다. 항상 예외를 던지는 함수와 무한 루프가 있는 함수입니다. 컴파일러는 이 주장을 검사합니다. never로 표기했는데 끝에 도달할 수 있는 함수는 오류 TS2534, A function returning 'never' cannot have a reachable end point입니다.

function fail(message: string): never {
  throw new Error(message);
}

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

추론은 문법에 따라 다릅니다. 예외만 던지는 함수 선언은 void를 반환한다고 추론되지만, 예외만 던지는 화살표 함수나 함수 표현식은 never로 추론됩니다.

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

좁히기가 호출을 막다른 길로 취급하는 것은 호출된 이름에 never를 반환하는 명시적 타입이 있을 때뿐입니다. 위의 fail처럼 : never로 표기한 함수 선언이거나, 타입 표기가 있는 변수 const fail: (m: string) => never = (m) => { throw new Error(m); }입니다. 추론된 never는 해당하지 않으며, 화살표 함수에만 표기하고 변수에는 표기하지 않은 const fail = (m: string): never => ...도 해당하지 않습니다.

never vs void

voidnever
함수가 끝남예아니요(예외를 던지거나 무한 반복)
런타임의 값undefined없음: 호출이 값을 만들지 않음
호출 뒤의 코드도달 가능도달 불가
다른 타입에 대입void, unknown, any에만모든 타입에
대표적인 용도콜백, 이벤트 핸들러, 부수 효과가 있는 함수fail(), assertNever(), 무한 루프

실질적인 차이는 좁히기에서 드러납니다. if (!user) fail("no user") 뒤에 컴파일러가 다음 줄에서 user가 정의되어 있다고 아는 것은 fail이 never를 반환할 때뿐입니다. 반환 타입이 void면 실행이 계속될 수 있다고 가정합니다.

never로 하는 완전성 검사

유니언에 대한 switch의 각 case는 값을 좁힙니다. 모든 멤버를 처리하고 나면 default에 남는 것은 never입니다. 그것을 never 변수에 대입하면 "모든 경우를 처리했다"는 사실을 컴파일러가 검증하게 됩니다.

이제 case를 추가하지 않고 유니언에 세 번째 멤버를 더해 보세요.

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

오류가 빠뜨린 멤버의 이름을 알려 줍니다. case "triangle": return (shape.base * shape.height) / 2;를 더하면 다시 컴파일됩니다. assertNever 헬퍼는 같은 검사를 재사용할 수 있게 만든 것이고, 그 안의 throw는 런타임에도 여전히 중요합니다. JSON이나 오래된 클라이언트에서 온 데이터에는 타입상 불가능한 kind가 들어 있을 수 있기 때문입니다. 이 패턴은 판별 유니언의 뼈대입니다.

never까지 좁혀지기

switch뿐 아니라 어떤 좁히기에서든 같은 일이 일어납니다. 모든 가능성이 배제되면 변수의 타입은 never입니다.

나중에 매개변수를 string | number | boolean | bigint로 넓히면 const nothing: never = x 줄이 오류가 되어, 수정이 필요한 함수를 가리킵니다.

유니언에서 never는 사라진다

never는 값의 공집합이므로 유니언에 더해도 아무것도 바뀌지 않습니다. string | never는 그냥 string입니다. 조건부 타입은 이렇게 유니언을 걸러 냅니다. never를 반환하는 분기는 그 멤버를 없앱니다.

내장 Exclude<T, U>와 Extract<T, U>가 정확히 이렇게 동작합니다. 교차 타입에서는 반대입니다. string & never는 never입니다.

불가능한 타입은 never가 된다

어떤 값도 만족할 수 없는 교차 타입은 never로 줄어듭니다.

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

B 값의 속성을 읽으면 이유가 보고됩니다: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. 만든 타입이 never로 드러나면 서로 모순되는 두 부분을 찾아보세요.

never, unknown, any

타입담는 값대입할 수 있는 곳받아들이는 것
unknown모든 값unknown과 any에만모든 것
any모든 값never를 제외한 모든 것모든 것
never값 없음모든 것never만

unknown은 타입 계층의 꼭대기이고 never는 바닥입니다. any는 계층에 아예 속하지 않고, 검사를 끌 뿐입니다.

자주 묻는 질문

TypeScript의 never 타입이란 무엇인가요?

never는 값이 하나도 없는 타입입니다. (다른 never를 빼면) 아무것도 대입할 수 없고, 모든 타입에 대입할 수 있습니다. 항상 예외를 던지거나 무한히 도는 함수의 반환 타입, 좁히기가 모든 가능성을 배제한 뒤의 변수 타입, string & number 같은 불가능한 타입의 결과로 나타납니다.

never와 void의 차이는 무엇인가요?

void를 반환하는 함수는 정상적으로 끝나고, 쓸모 있는 값을 반환하지 않을 뿐입니다(런타임에는 undefined를 반환합니다). never를 반환하는 함수는 아예 끝나지 않습니다. 예외를 던지거나 영원히 실행됩니다. never 함수를 호출한 뒤의 코드는 도달할 수 없고, TypeScript는 좁히기에서 그렇게 취급합니다.

TypeScript에서 완전성 검사는 어떻게 하나요?

유니언에 대한 switch의 default 분기에서 값을 never 타입 변수에 대입하거나, 예외를 던지는 assertNever(value: never): never 함수에 넘기세요. 모든 경우를 처리했다면 그 자리에서 값은 never이고 코드가 컴파일됩니다. 빠진 경우가 있으면 컴파일러가 그 멤버를 never에 대입할 수 없다고 보고합니다.

왜 내 타입이 never가 되나요?

대개 TypeScript가 모든 선택지를 좁혀서 없앴기 때문이거나(예를 들어 string | number에 대해 typeof x === "string"과 typeof x === "number"를 모두 확인한 뒤), 교차 타입이 불가능하기 때문입니다. string & number나, 공유하는 속성의 리터럴 타입이 서로 충돌하는 두 객체 타입이 그렇습니다. 에디터에서 타입에 마우스를 올려 어느 단계에서 생겼는지 확인하세요.

"is not assignable to type never"는 무슨 뜻인가요?

never만 허용되는 자리에 실제 값을 넣으려 했다는 뜻입니다. 완전성 검사에서는 유니언 멤버 하나를 처리하지 않았다는 뜻입니다. 다른 곳에서는 배열이 never[]로 추론되었거나 교차 타입이 never로 무너졌다는 뜻인 경우가 많습니다. 타입 표기를 더하거나 충돌하는 타입을 고치세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기