타입 내로잉

타입 내로잉 (Narrowing)

타입스크립트는 타입 가드와 할당을 분석하면서, 값을 더 구체적인 타입으로 좁혀가요. 이 과정을 내로잉(narrowing)이라고 하는데요, 오늘은 이 함수가 코드 안에서 실제로 어떻게 흐르는지 함께 살펴볼게요. 단순해 보이지만 그 안에는 제법 많은 일이 벌어져요.

출처: TypeScript 공식문서

본문

Narrowing

padLeft라는 함수가 있다고 상상해볼게요.

function padLeft(padding: number | string, input: string): string {
  throw new Error("Not implemented yet!");
}

paddingnumber라면 그 값만큼의 공백을 input 앞에 붙이는 걸 의도해요. paddingstring이라면 그 문자열을 그대로 앞에 붙이면 되고요. 먼저 paddingnumber가 들어왔을 때의 로직을 구현해보죠.

function padLeft(padding: number | string, input: string): string {
  return " ".repeat(padding) + input;
  // Argument of type 'string | number' is not assignable to parameter of type 'number'.
  //   Type 'string' is not assignable to type 'number'. (2345)
}

이런, padding 부분에서 에러가 나네요. 타입스크립트가 number만 받는 repeat 함수에 number | string 타입 값을 넘기고 있다고 경고하는 거예요. 아직 paddingnumber인지 확인하지 않았고, string인 경우도 처리하지 않았으니 그걸 그대로 해줄게요.

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

이게 평범한 자바스크립트 코드처럼 보인다면, 그게 바로 핵심이에요. 우리가 넣은 타입 주석을 빼면 이 타입스크립트 코드는 자바스크립트와 똑같아요. 타입스크립트의 타입 시스템은 타입 안정성을 얻기 위해 몸을 꼬지 않아도 되는, 평범한 자바스크립트를 그대로 쓰게 해주는 걸 목표로 해요.

그런데 겉으로는 대수롭지 않아 보여도, 이 안에서는 꽤 많은 일이 일어나요. 타입스크립트는 정적 타입으로 런타임 값을 분석하듯이, 자바스크립트의 런타임 제어 흐름 구조 위에 타입 분석을 얹어요. if/else, 조건부 삼항, 루프, truthiness 검사 같은 것들이 전부 타입에 영향을 줄 수 있거든요.

if 검사 안에서 타입스크립트는 typeof padding === "number"라는 코드를 보고, 이를 타입 가드(type guard)라는 특별한 형태로 이해해요. 그리고 프로그램이 실제로 거칠 수 있는 실행 경로를 따라가면서, 특정 위치에서 값이 가질 수 있는 가장 구체적인 타입을 분석해요. 이렇게 특별한 검사(타입 가드)와 할당을 살펴보며, 선언된 타입보다 더 구체적인 타입으로 다듬어 가는 과정을 바로 내로잉(narrowing)이라고 해요. 많은 에디터에서 이 타입이 변하는 걸 그대로 관찰할 수 있는데, 예시에서도 그걸 확인해볼게요.

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
    // (parameter) padding: number
  }
  return padding + input;
  // (parameter) padding: string
}

타입스크립트가 내로잉을 위해 이해하는 구조는 몇 가지가 있어요. 하나씩 살펴볼게요.

typeof 타입 가드 (typeof type guards)

앞서 본 것처럼 자바스크립트는 typeof 연산자로 런타임 값의 타입에 대한 아주 기본적인 정보를 얻을 수 있어요. 타입스크립트는 이 연산자가 특정 문자열 집합을 돌려주리라 기대해요.

"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"

padLeft에서 본 것처럼 이 연산자는 자바스크립트 라이브러리에서 꽤 자주 등장하는데, 타입스크립트는 이를 이해해서 각 분기에서 타입을 내로잉할 수 있어요. 타입스크립트에서 typeof가 돌려준 값과 비교하는 것은 타입 가드예요. 타입스크립트는 typeof가 서로 다른 값에 어떻게 동작하는지를 알고 있기 때문에, 자바스크립트의 몇 가지 특이한 점도 함께 알아요. 예를 들어 위 목록을 보면 typeofnull이라는 문자열을 돌려주지 않는다는 걸 알 수 있어요. 다음 예시를 볼게요.

function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
      // 'strs' is possibly 'null'. (18047)
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing
  }
}

printAll 함수에서 strs가 객체인지 확인해 배열 타입인지 알아보려고 했어요(여기서 짚어두면 좋은 게, 자바스크립트에서 배열은 객체 타입이에요). 그런데 자바스크립트에서 typeof null이 실제로 "object"라는 사실이 밝혀져요. 역사 속에서 벌어진 불운한 사고 중 하나죠. 경험이 많은 분이라면 놀랍지 않겠지만, 모든 사람이 자바스크립트에서 이걸 겪어본 건 아니에요. 다행히 타입스크립트는 strs가 단순히 string[]가 아니라 string[] | null로만 내로잉됐다는 걸 알려줘요.

이쯤에서 "truthiness" 검사로 자연스럽게 넘어가볼게요.

Truthiness 내로잉 (Truthiness narrowing)

"truthiness"는 사전에 없는 단어일 수 있는데, 자바스크립트에서는 정말 많이 듣게 되는 개념이에요. 자바스크립트에서는 조건문, &&, ||, if 문, 부정(!) 등 어디에든 어떤 표현식이든 쓸 수 있어요. 예를 들어 if 문은 조건이 항상 boolean 타입일 거라고 기대하지 않아요.

function getUsersOnlineMessage(numUsersOnline: number) {
  if (numUsersOnline) {
    return `There are ${numUsersOnline} online now!`;
  }
  return "Nobody's here. :(";
}

자바스크립트에서 if 같은 구조는 먼저 조건을 boolean으로 "강제 변환(coerce)"해서 해석하고, 그 결과가 true인지 false인지에 따라 분기를 골라요. 다음 값들은 전부 false로 강제 변환되고, 나머지 값들은 true로 변환돼요.

0
NaN
"" // the empty string
0n // the bigint version of zero
null
undefined

언제든 Boolean 함수를 통과시키거나, 더 짧은 이중 부정으로 값을 boolean으로 강제 변환할 수 있어요. 후자는 타입스크립트가 좁은 리터럴 타입 true를 추론한다는 장점이 있어요(전자는 boolean 타입으로 추론해요).

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
// This kind of expression is always truthy. (2872)

이 동작을 활용하는 건 꽤 흔한데, 특히 null이나 undefined 같은 값을 걸러낼 때 유용해요. 우리의 printAll 함수에 적용해볼게요.

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

strs가 truthy인지 확인하면서 아까의 에러가 사라진 걸 알 수 있어요. 이렇게 하면 코드를 실행할 때 다음과 같은 무서운 에러는 최소한 막을 수 있어요.

TypeError: null is not iterable

다만 명심할 게 있어요. 원시 타입에 대한 truthiness 검사는 자주 실수하기 쉬워요. printAll을 다르게 작성한 예를 볼게요.

function printAll(strs: string | string[] | null) {
  // !!!!!!!!!!!!!!!!
  // DON'T DO THIS!
  // KEEP READING
  // !!!!!!!!!!!!!!!!
  if (strs) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
    }
  }
}

함수 본문 전체를 truthy 검사로 감쌌는데, 여기엔 미묘한 단점이 있어요. 빈 문자열("")의 경우를 더 이상 올바르게 처리하지 못하게 될 수 있어요. 타입스크립트가 여기서 우리를 해치진 않지만, 자바스크립트에 익숙하지 않은 분이라면 이 동작을 알아둘 필요가 있어요. 타입스크립트는 종종 버그를 일찍 잡아주지만, 값을 아무것도 하지 않겠다고 선택하면 타입스크립트가 지나치게 간섭하지 않는 이상 할 수 있는 게 제한적이거든요. 원한다면 린터(linter)로 이런 상황을 처리하도록 할 수도 있어요.

truthiness 내로잉에 대해 마지막으로 하나 더, !를 이용한 부정은 부정된 분기에서 해당 값을 걸러내요.

function multiplyAll(
  values: number[] | undefined,
  factor: number
): number[] | undefined {
  if (!values) {
    return values;
  } else {
    return values.map((x) => x * factor);
  }
}

동등성 내로잉 (Equality narrowing)

타입스크립트는 switch 문과 ===, !==, ==, != 같은 동등성 검사도 타입 내로잉에 사용해요. 예를 들면 이렇게요.

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();
    y.toLowerCase();
  } else {
    console.log(x);
    console.log(y);
  }
}

위 예시에서 xy가 서로 같다고 확인했을 때, 타입스크립트는 두 타입도 같아야 한다는 걸 알았어요. xy가 모두 가질 수 있는 공통 타입이 string뿐이므로, 첫 번째 분기에서 xy는 반드시 string이어야 한다는 걸 타입스크립트가 파악해요.

변수가 아니라 특정 리터럴 값과 비교하는 것도 동작해요. truthiness 내로잉에서 공백 문자열을 제대로 처리하지 못했던 printAll 함수를 생각해볼게요. 그 대신 null만 막는 구체적인 검사를 할 수 있는데, 타입스크립트는 여전히 strs의 타입에서 null을 올바르게 제거해줘요.

function printAll(strs: string | string[] | null) {
  if (strs !== null) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
        // (parameter) strs: string[]
      }
    } else if (typeof strs === "string") {
      console.log(strs);
      // (parameter) strs: string
    }
  }
}

자바스크립트의 느슨한 동등성 검사, ==!=도 올바르게 내로잉돼요. 다소 낯설 수 있는데, x == null이 참인지 확인하는 것은 값이 정확히 null인지만 보는 게 아니라, undefined일 가능성도 함께 보는 거예요. == undefined도 마찬가지로 값이 null이거나 undefined인지 확인해요.

interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number) {
  // Remove both 'null' and 'undefined' from the type.
  if (container.value != null) {
    console.log(container.value);
    // (property) Container.value: number
    // Now we can safely multiply 'container.value'.
    container.value *= factor;
  }
}

in 연산자 내로잉 (The in operator narrowing)

자바스크립트에는 객체나 그 프로토타입 체인에 특정 이름의 속성이 있는지 판단하는 연산자가 있어요. 바로 in 연산자예요. 타입스크립트는 이를 잠재적 타입 후보를 좁히는 방법으로 고려해요.

예를 들어 "value" in x라는 코드가 있는데, "value"는 문자열 리터럴이고 x는 유니온 타입이라고 해볼게요. "참" 분기는 x를, value 속성을 옵셔널 또는 필수로 갖는 타입들로 좁혀주고, "거짓" 분기는 value 속성을 옵셔널로 갖거나 아예 갖지 않는 타입들로 좁혀줘요.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }

  return animal.fly();
}

다시 강조하면, 옵셔널 속성은 내로잉 시 양쪽 모두에 존재해요. 예를 들어 사람은 (적절한 장비만 있으면) 수영도 하고 날 수도 있으니, in 검사의 양쪽 모두에 나타나야 해요.

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };

function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) {
    animal;
    // (parameter) animal: Fish | Human
  } else {
    animal;
    // (parameter) animal: Bird | Human
  }
}

instanceof 내로잉 (instanceof narrowing)

자바스크립트에는 어떤 값이 다른 값의 "인스턴스"인지 확인하는 연산자가 있어요. 더 구체적으로, 자바스크립트에서 x instanceof Foox의 프로토타입 체인에 Foo.prototype이 포함되는지 확인해요. 여기서 깊게 파고들진 않겠지만, 클래스를 다룰 때 이걸 더 많이 보게 될 거예요. new로 만들 수 있는 대부분의 값에 여전히 유용해요. 짐작했겠지만, instanceof도 타입 가드이고, instanceof로 보호된 분기에서 타입스크립트는 타입을 내로잉해요.

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
    // (parameter) x: Date
  } else {
    console.log(x.toUpperCase());
    // (parameter) x: string
  }
}

할당 (Assignments)

앞서 언급했듯이, 어떤 변수에 할당할 때 타입스크립트는 할당의 오른쪽을 보고 왼쪽을 적절히 내로잉해요.

let x = Math.random() < 0.5 ? 10 : "hello world!";
// let x: string | number
x = 1;
console.log(x);
// let x: number
x = "goodbye!";
console.log(x);
// let x: string

이 할당들이 전부 유효하다는 걸 눈여겨보세요. 첫 할당 후 관찰되는 x의 타입이 number로 바뀌었지만, 여전히 xstring을 할당할 수 있어요. 왜냐하면 x의 선언된 타입, 즉 x가 처음 가졌던 타입이 string | number이고, 할당 가능성은 항상 선언된 타입을 기준으로 검사되기 때문이에요.

xboolean을 할당했다면 에러를 봤을 거예요. 그건 선언된 타입에 포함되지 않으니까요.

let x = Math.random() < 0.5 ? 10 : "hello world!";
// let x: string | number
x = 1;
console.log(x);
// let x: number
x = true;
// Type 'boolean' is not assignable to type 'string | number'. (2322)
console.log(x);
// let x: string | number

제어 흐름 분석 (Control flow analysis)

지금까지는 타입스크립트가 특정 분기 안에서 어떻게 내로잉하는지의 기본적인 예시를 살펴봤어요. 그런데 단순히 모든 변수에서 시작해 if, while, 조건문 등에서 타입 가드를 찾아내는 것보다 더 많은 일이 일어나요. 예를 들어볼게요.

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

padLeft는 첫 번째 if 블록 안에서 값을 반환해요. 타입스크립트는 이 코드를 분석해서, paddingnumber인 경우에는 나머지 본문(return padding + input;)에 도달할 수 없다는 걸 파악했어요. 그 결과 함수의 나머지 부분에서 padding의 타입에서 number를 제거할 수 있었죠(string | number에서 string으로 내로잉). 이렇게 도달 가능성(reachability)을 기반으로 코드를 분석하는 것을 제어 흐름 분석(control flow analysis)이라고 해요. 타입스크립트는 이 흐름 분석을 사용해 타입 가드와 할당을 만날 때마다 타입을 내로잉해요. 변수를 분석할 때 제어 흐름은 계속해서 갈라졌다가 다시 합쳐질 수 있고, 그 지점마다 변수가 관찰되는 타입이 달라질 수 있어요.

function example() {
  let x: string | number | boolean;

  x = Math.random() < 0.5;
  console.log(x);
  // let x: boolean

  if (Math.random() < 0.5) {
    x = "hello";
    console.log(x);
    // let x: string
  } else {
    x = 100;
    console.log(x);
    // let x: number
  }

  return x;
  // let x: string | number
}

타입 서술 함수 사용하기 (Using type predicates)

지금까지는 기존 자바스크립트 구조로 내로잉을 처리해 왔어요. 그런데 때로는 코드 전체에서 타입이 어떻게 변하는지 더 직접적으로 제어하고 싶을 때가 있어요.

사용자 정의 타입 가드를 정의하려면, 반환 타입이 타입 서술(type predicate)인 함수를 정의하기만 하면 돼요.

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

이 예시에서 pet is Fish가 우리의 타입 서술이에요. 서술은 parameterName is Type 형태인데, parameterName은 반드시 현재 함수 시그니처의 매개변수 이름이어야 해요.

isFish를 어떤 변수로 호출할 때마다, 원래 타입이 호환된다면 타입스크립트는 그 변수를 해당 특정 타입으로 내로잉해요.

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();

if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

타입스크립트가 if 분기에서 petFish라는 것뿐 아니라, else 분기에서 Fish가 아니므로 반드시 Bird라는 것도 안다는 점을 눈여겨보세요.

Fish | Bird 배열을 걸러 Fish 배열을 얻는 데 타입 가드 isFish를 사용할 수도 있어요.

const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];

// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});

게다가 클래스는 this is Type을 사용해 자신의 타입을 내로잉할 수도 있어요. (this-based type guards)

단언 함수 (Assertion functions)

타입은 단언 함수(assertion functions)를 이용해서도 내로잉할 수 있어요.

판별 유니온 (Discriminated unions)

지금까지 본 예시들은 대부분 string, boolean, number 같은 단순한 타입의 단일 변수를 내로잉하는 데 초점을 맞췄어요. 흔한 경우지만, 자바스크립트에서는 대부분 조금 더 복잡한 구조를 다루게 돼요.

동기를 위해 원(circle)과 정사각형(square) 같은 도형을 표현하려 한다고 상상해볼게요. 원은 반지름(radius), 정사각형은 한 변의 길이(sideLength)를 추적해요. 어떤 도형인지 구분할 필드를 kind라고 부르기로 해요. Shape를 정의하는 첫 시도는 이렇게 됩니다.

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

이 도형을 원으로 볼지 정사각형으로 볼지 알려주기 위해 "circle""square"라는 문자열 리터럴 타입의 유니온을 쓰고 있어요. string 대신 "circle" | "square"를 씀으로써 오타 문제를 피할 수 있어요.

function handleShape(shape: Shape) {
  // oops!
  if (shape.kind === "rect") {
    // This comparison appears to be unintentional because the types
    // '"circle" | "square"' and '"rect"' have no overlap. (2367)
    // ...
  }
}

원이든 정사각형이든 구분해서 올바른 로직을 적용하는 getArea 함수를 작성할 수 있어요. 먼저 원을 다뤄볼게요.

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
  // 'shape.radius' is possibly 'undefined'. (18048)
}

strictNullChecks(strictNullChecks)를 켜면 이 코드에서 에러가 나요. radius가 정의되지 않았을 수 있으니 적절한 반응이에요. 그런데 kind 속성에 적절한 검사를 수행하면 어떨까요?

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
    // 'shape.radius' is possibly 'undefined'. (18048)
  }
}

음, 타입스크립트는 여전히 어떻게 해야 할지 모르겠어요. 우리가 타입 체커보다 값에 대해 더 많이 아는 지점에 도달했죠. radius가 확실히 존재한다는 것을 알려주기 위해 shape.radius 뒤에 !를 붙이는 널 아님 단언(non-null assertion)을 시도해볼 수 있어요.

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius! ** 2;
  }
}

그런데 이 방식은 이상적이지 않아요. shape.radius가 정의되어 있다는 걸 타입 체커에게 확신시키려고 그 ! 단언으로 조금 소리를 질러야 했는데, 코드를 옮기기 시작하면 이런 단언은 실수하기 쉬워요. 게다가 strictNullChecks 밖에서는 (읽을 때 옵셔널 속성이 항상 존재한다고 그냥 가정하므로) 어쨌든 그 필드들에 실수로 접근할 수 있게 돼요. 분명 더 나은 방법이 있어요.

Shape 인코딩의 문제는, 타입 체커가 kind 속성만으로 radiussideLength가 존재하는지 알 방법이 없다는 거예요. 우리가 아는 것을 타입 체커에게 전달해야 해요. 이 점을 염두에 두고 Shape를 다시 정의해볼게요.

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

여기서 Shapekind 속성의 값이 서로 다른 두 타입으로 제대로 분리했어요. 그리고 radiussideLength는 각 타입에서 필수 속성으로 선언했고요.

Shaperadius에 접근하려고 하면 어떤 일이 벌어지는지 볼게요.

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
  // Property 'radius' does not exist on type 'Shape'.
  //   Property 'radius' does not exist on type 'Square'. (2339)
}

Shape 정의와 마찬가지로 이것도 여전히 에러예요. radius가 옵셔널일 때는 속성이 존재하는지 타입스크립트가 알 수 없어서(strictNullChecks를 켠 상태에서) 에러가 났었죠. 이제 Shape가 유니온이 되면서, 타입스크립트는 shapeSquare일 수도 있는데 Square에는 radius가 정의되어 있지 않다고 알려줘요. 두 해석 모두 옳지만, Shape를 유니온으로 인코딩한 경우에만 strictNullChecks 설정과 무관하게 에러가 나요.

그럼 다시 kind 속성을 검사해보면 어떨까요?

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
    // (parameter) shape: Circle
  }
}

에러가 사라졌죠! 유니온의 모든 타입이 리터럴 타입의 공통 속성을 포함하면, 타입스크립트는 이를 판별 유니온(discriminated union)으로 간주하고 유니온의 멤버들을 내로잉할 수 있어요.

이 경우 kind가 바로 그 공통 속성이었어요(Shape의 판별 속성(discriminant property)이라 부르는 것). kind 속성이 "circle"인지 확인하는 것만으로, kind 속성이 "circle" 타입이 아닌 Shape의 모든 타입이 제거됐어요. 그게 shapeCircle 타입으로 내로잉해 준 거예요.

같은 검사가 switch 문에서도 동작해요. 이제 지저분한 ! 널 아님 단언 없이 완전한 getArea를 작성해볼게요.

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
      // (parameter) shape: Circle
    case "square":
      return shape.sideLength ** 2;
      // (parameter) shape: Square
  }
}

여기서 중요한 것은 Shape의 인코딩이었어요. CircleSquare가 특정 kind 필드를 가진 정말 서로 다른 두 타입이라는 것을 타입스크립트에 전달하는 게 핵심이었죠. 그렇게 하면 우리가 평소에 작성했을 자바스크립트와 다를 바 없는, 타입 안전한 타입스크립트 코드를 쓸 수 있어요. 거기서부터 타입 시스템이 "올바른" 일을 해서 switch 문의 각 분기에서 타입을 알아냈어요.

한 가지 곁다리로, 위 예시를 직접 실행해보면서 return 키워드를 몇 개 지워보세요. switch 문에서 의도치 않게 다른 절로 넘어가는(fall-through) 버그를 타입 체크가 얼마나 잘 막아주는지 볼 수 있을 거예요.

판별 유니온은 원과 정사각형 이야기보다 훨씬 더 넓게 쓰여요. 네트워크로 메시지를 보낼 때(클라이언트/서버 통신)나 상태 관리 프레임워크에서 변화(mutation)를 인코딩할 때처럼, 자바스크립트에서 어떤 종류의 메시지 체계든 표현하는 데 아주 좋아요.

never 타입 (The never type)

내로잉을 하다 보면 유니온의 가능성을 모두 제거해서 아무것도 남지 않는 지점까지 줄일 수 있어요. 그런 경우 타입스크립트는 존재해서는 안 되는 상태를 나타내기 위해 never 타입을 사용해요.

철저한 검사 (Exhaustiveness checking)

never 타입은 모든 타입에 할당 가능하지만, 반대로 어떤 타입도 never에 할당할 수 없어요(never 자신은 제외). 즉 내로잉과 함께 never가 나타나는 것을 활용해서 switch 문에서 철저한 검사(exhaustive checking)를 할 수 있다는 뜻이에요.

예를 들어 getAreadefault를 추가하고 shapenever에 할당하려 하면, 모든 가능한 경우가 처리됐을 때는 에러가 발생하지 않아요.

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Shape 유니온에 새 멤버를 추가하면, 타입스크립트 에러가 발생해요.

interface Triangle {
  kind: "triangle";
  sideLength: number;
}

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      // Type 'Triangle' is not assignable to type 'never'. (2322)
      return _exhaustiveCheck;
  }
}

더 알아보기