[TypeScript] void vs undefined

September 18, 2022

아래의 코드는 우아한테크코스 지원 플랫폼의 타입스크립트 전환 작업을 진행하면서 마주쳤던 이슈입니다. axios interceptors가 성공 응답을 처리할 때 body가 없는 경우 리턴 타입을 다음 둘 중에서 고민하고 있었습니다.

type InterceptedResponse = { data: unknown } | void; // 1
type InterceptedResponse = { data: unknown } | undefined; // 2

axios.interceptors.response.use<InterceptedResponse>(
  function (response) {
    const hasResponseData = Object.prototype.hasOwnProperty.call(response.data, "body");

    return hasResponseData ? Promise.resolve({ data: response.data["body"] }) : Promise.resolve();
  },
  funtion (error) {
    // ...
  }
)

Promise<void>Promise<undefined>나 awaited된 값은 undefined일테니 같을 것이라 생각했는데 타입을 undefined로 지정하면 에러가 발생했습니다. void는 ‘undefined를 반환한다’ 이상의 의미가 있는 것일까요?

자바스크립트의 void

void라는 키워드는 다른 프로그래밍 언어를 경험해봤다면 익숙할텐데요, 자바스크립트에도 void 연산자가 있습니다. ECMAScript에 따르면 void는:

Syntax: void UnaryExpression

  1. Let expr be ? Evaluation of UnaryExpression.
  2. Perform ? GetValue(expr).
  3. Return undefined.

다시 말해 void 연산자는,

  1. void 다음의 표현식을
  2. 평가하고
  3. undefined를 반환합니다.

컴퓨터 과학에서의 평가란 표현식 또는 함수(서브루틴)의 결과값을 계산하는 것입니다. 값을 계산한다는 것은 코드를 실행한다는 의미와 동일합니다. void는 표현식 또는 함수를 실행한 뒤 반환된 값을 무시할 때 사용됩니다.

대표적인 사용례는 undefined 원시값 생성, IIFE입니다. 하지만 ES6부터 지원되는 letconst를 이용하면 블록 스코핑이 가능하기 때문에 IIFE를 위한 void의 필요성도 줄었습니다.

타입스크립트의 void

void represents the return value of functions which don’t return a value.

아무 것도 반환하지 않는 함수의 반환 타입은 void입니다. 타입스크립트에서 늘 그렇게 사용해왔고 굳이 명시하지 않더라도 알아서 void로 추론합니다. 그리고 그러한 함수들의 반환 값은 undefined로 평가됩니다. 그 값을 undefined 타입으로 지정한 변수에 할당하면 어떻게 될까요?

function voidFunc(): void {}
const foo: undefined = voidFunc() // Type 'void' is not assignable to type 'undefined'.

분명 반환된 값은 undefined인데 void 타입은 undefined에 할당할 수 없다고 합니다. void와 undefined는 차이가 있다는 말이네요.

void 타입의 표현식은 truthiness를 테스트할 수 없습니다.

An expression of type ‘void’ cannot be tested for truthiness.

void 함수의 결과값의 truthiness를 검증하는 경우 위의 에러 메시지가 출력됩니다.

function voidFunc() {}

const voidResult = voidFunc()

voidResult && console.log(voidResult)
// An expression of type 'void' cannot be tested for truthiness.

undefined는 truthiness를 검증할 수 있습니다.

function undefinedFunc(): undefined {
  return
}

const undefinedResult = undefinedFunc()

undefinedResult && console.log(undefinedResult)

Substitutability

void를 반환하는 Contextual 함수 타이핑(식의 한변에만 타입을 지정하는 것)은 함수에게 아무것도 반환하지 않을 것을 강제하지 않습니다. 실제로도 아래 f1, f2, f3와 같이 값을 반환해도 아무런 타입 에러가 발생하지 않습니다. 다만 반환된 값이 무시됩니다.

type voidFunc = () => void

const f1: voidFunc = () => {
  return true
}

const f2: voidFunc = () => true

const f3: voidFunc = function () {
  return true
}

console.log(f1()) // undefined
console.log(f2()) // undefined
console.log(f3()) // undefined

때문에 Array.prototype.forEach는 인자로 void를 반환하는 콜백 함수를 기대하지만 number 를 반환하는 Array.prototype.push 를 받더라도 유효합니다.

// Array<number>.push(...): **number**
// Array<number>.forEach(callbackfn: (…) => **void**, …): void

const src = [1, 2, 3]
const dst = [0]

src.forEach(el => dst.push(el))
// OK

반대로 undefined를 반환하는 콜백 함수를 기대하는 경우 에러가 발생합니다.

declare function undefinedForEach<T>(
  list: T[],
  callbackfn: (el: T) => undefined
): void

const src = [1, 2, 3]
const dst = [0]

undefinedForEach(src, el => dst.push(el))
// Type 'number' is not assignable to type 'undefined'.

Generic Return Type

반환값이나 인자에서 제네릭을 사용해 타이핑하는 경우 void는 아무 것도 반환하지 않거나 주어지지 않음을 나타낼 때 유용합니다.

const api = {
  whatever: () => fetch("url"),
}

async function promiseReturnsResponse() {
  return await api.whatever()
}

async function promiseReturnsNothing(): Promise<void> {
  return await api.whatever() // Type 'Response' is not assignable to type 'void'.
}

위 예제의 두 비동기 함수를 보면, API 요청이 fulfilled되면 promiseReturnsString은 string을, promiseReturnsNothing은 아무것도 반환하지 않습니다. promiseReturnsNothing에서 리턴하려고 하는 경우 에러가 발생합니다.

결론

타입스크립트에서 void는 모호하고, 때로는 unknown처럼 동작합니다. 하지만 그것은 타입스크립트가 ‘void로 지정한 값은 사용할 수 없음’을 표현하기 위해 의도한 동작입니다. 변수의 경우 undefined를 제외한 값을 할당할 수 없고 함수의 인자의 경우 caller가 반환값을 무시함을 뜻합니다.

참고


우정민

웹 개발, 프론트엔드