Never 타입

Never

Youtube: Never 타입에 대한 동영상 강의

Egghead: Never 타입에 대한 동영상 강의

프로그래밍 언어 디자인에는 코드 흐름 분석 을 수행할 때 자연스럽게 결과로 나오는 바닥 타입이라는 개념이 있습니다. TypeScript도 코드 흐름 분석 을 하기 때문에 (😎) 발생 가능성이 없는 것들을 표현할 방법이 필요합니다.

TypeScript가 이 바닥 타입을 나타내기 위해 사용하는 것이 never 타입입니다. 이 타입이 발생하는 경우들:

  • 리턴하지 않는 함수 (e.g. 함수 내용에 while(true){}가 들어 있는 경우)

  • 항상 예외를 던지는 함수 (e.g. function foo(){throw new Error('Not Implemented')} 에서 foo의 리턴 타입이 never)

당연히 이 어노테이션을 프로그래머가 직접 사용할 수도 있습니다

let foo: never; // 오케이

그렇지만, never 타입에는 다른 never 타입만 할당 가능합니다. 예를 들면:

let foo: never = 123; // 오류: number 타입은 never에 할당할 수 없음

// 오케이, 함수의 리턴 타입이 `never`
let bar: never = (() => { throw new Error(`Throw my hands in the air like I just don't care`) })();

좋습니다. 그러면 핵심 사용 방법으로 넘어가죠 :)

사용 사례: 빠짐없는 검사(Exhaustive Check)

발생 불가능한 상황(never 컨텍스트)에서 never 함수를 호출할 수 있습니다.

function foo(x: string | number): boolean {
  if (typeof x === "string") {
    return true;
  } else if (typeof x === "number") {
    return false;
  }

  // never 타입이 없다면 오류 발생 :
  // - 코드 경로 중에 값을 반환하지 않는 경로 존재 (strictNullChecks)
  // - 또는 접근 불가능한 코드 검출
  // 하지만 TypeScript는 `fail` 함수는 `never` 리턴임을 알 수 있고
  // 런타임 안전 / 빠짐없는 검사를 위해 이런 함수를 호출할 수 있음.
  return fail("Unexhaustive!");
}

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

그리고 never에는 never만 할당할 수 있기 때문에 컴파일 시간의 빠짐없는 검사 목적으로 사용할 수 있습니다. 이 내용은 구별된 유니온 단원에 나와 있습니다.

void와 혼동

함수가 매끄럽게 종료하지 않을 때 never가 반환된다고 하면 직관적으로 이것은 void 같은 것이라고 생각하기 쉽습니다. 그렇지만 void는 하나의 단위이고 never 모순(falsum)입니다.

아무것도 반환하지 않는 함수는 단위 void를 반환하는 것입니다. 하지만 영원히 리턴하지 않는 함수 (또는 항상 throw하는 함수)는 never입니다. void는 할당이 가능한 타입이지만 (strictNullCheckings를 끄면) never는 절대 never 이외에는 할당할 수 없습니다.

never 반환 함수의 타입 추론

함수 선언시 TypeScript는 기본으로 void를 가정합니다, 아래 처럼:

// 추론된 리턴 타입: void
function failDeclaration(message: string) {
  throw new Error(message);
}

// 추론된 리턴 타입: never
const failExpression = function(message: string) {
  throw new Error(message);
};

물론 명시적인 어노테이션을 추가하면 고칠 수 있습니다:

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

이렇게 하는 핵심 이유는 실제 JavaScript 코드와의 호환성 유지입니다:

class Base {
    overrideMe() {
        throw new Error("You forgot to override me!");
    }
}

class Derived extends Base {
    overrideMe() {
        // Code that actually returns here
    }
}

Base.overrideMe 호출의 경우.

실제 TypeScript에서는 abstract 함수를 써서 이 문제를 해결할 수 있지만 이 타입 추론은 호환성을 위해 유지되고 있습니다.

Last updated