TypeScript - Handbook(Narrowing)

2021. 5. 30. 15:15프론트엔드/Typescript

728x90

내로윙(Narrowing)이란?

Narrowing은 실제로 선언된 타입들에 대하여 더 구체적인 타입에 대해 처리하는 것.
예를 들어 'name: string | number'라고 한다면,
string일 때와 number일 때를 구분하여 처리하는 과정을 말한다. 

Narrowing에 의의는 각 타입에 대한 처리를 분명하게 하여,
에러가 나지 않는 안전한 코드를 만드는 것이라 할 수 있다.


typeof를 이용한 타입 가드

원시 타입 (primitives)을 체크하기 위해서는 typeof를 사용합니다

function printAll(strs: string | string[] | null) {
  if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing
  }
}

typeof로 구분 가능한 type은 다음과 같습니다

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


Truthiness Narrowing

글에서도 Truthiness는 일상에서 사용하는 단어는 아니라고 한다.
&&, ||, if, ! 등을 이용하여 구분하는 것을 말한다.
if문에서는 어떤 타입이건 boolean타입이 되도록 coerce(강제)한다.
그래서 다음과 같이 처리를 할 수 있다.

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

numUsersOnline만 if문 안에 있지만, 0이면 false, 그 이외의 값에 대해서는 true가 된다.

다른 예로는 이런 게 있다.

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

이렇게 되면 에러가 뜬다.
왜냐하면 if( typeof strs === "object")는
string[]뿐만 아니라 null도 통과시킨다.


등호를 이용한 내로윙 (Equality Narrowing)

자바스크립트에는 ==말고도 ===(자매품 !==)이 있다.
이 차이는 다음과 같다.

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

function multiplyValue(container: Container, factor: number) {
  // Null과 undefined가 동시에 없어짐
  if (container.value != null) {
    console.log(container.value);
  }
  
  // null만 없어짐
  if (constainer.value !== null) {
    //undefined일 수도 있음
  	console.log(container.value)
  }
}

null, undefined외에도 사용은 다양하다.


const a: number = 5;
const b: string = "5";

function check(a: string | number, b: string | boolean) {
    if (a == b) {
        console.log("a");
    }

    if (a === b) {
        console.log("b")
    }
}

결과는 a만 출력이 된다. javascript에서는 5 == "5"도 true로 처리가 된다.
하지만 ===을 사용하면 타입과 값 둘다 같아야 true로 처리가 된다.


in 연산자를 이용한 내로윙 (in operator narrowing)

오브젝트를 체크할 때, 해당 오브젝트 안에 특정 키가 있는지 확인할 수 있다.

type Marine = { steampack: () => void };
type FireBat = { steampack: () => void };
type Ghost = { lockdown: () => void };

function useSteampack(unit: Marine | FireBat | Ghost) {
    if ("steampack" in unit) {
        return unit.steampack();
    } else {
        // do nothing
    }
}

스타크래프트에서 마린과 파이어뱃,  고스트를 한 부대로 지정하고서 스팀팩을 사용하는 경우를 생각해보자.
고스트는 스팀팩이 없기 때문에, unit.steampack()을 사용하면 에러가 날 것이다.
다음과 같이 steampack이 있는 경우에만 실행되도록 하면 된다.


instanceof 연산자를 이용한 내로윙 (instanceof Narrowing)

원시 타입(primitives)은 typeof로 처리했다.
그렇다면 객체는 어떻게 처리할까?

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

다음과 같이 instanceof 연산자를 이용해서 해당 클래스의 객체인지 확인한다.
이는 기존 Javascript의 Foo.protoType과도 같다고 한다.


대입을 통한 내로윙 (Assignments)

예제가 재미있어서 가져와봤다.

let x = Math.random() < 0.5 ? 10 : "hello world!";

뭐 이런 슈뢰딩거 같은 코드가 다 있지 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
아무튼 이런 상태라면 type은 'string | number'가 된다.

그리고 여기에 한 종류로 대입을 하면 해당 타입으로 인식을 한다.

// x: number
x = 1;

// x: string
x = "goodbye!";

 is를 이용하는 방법 (type predicates)

어떤 변수가 어떤 타입인지 알고 싶다면 다음과 같이 해보자.

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

이 함수의 최종 목적은 반환 타입인 'pet is Fish'라고 할 수 있다.

이걸 어디다 쓰냐고 할 수 있는데, iteration을 하는 상황이나 if를 이용한 분기에 사용할 수 있다ㅏ.

// if 분기에 사용하기
let pet = getSmallPet();

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

// iterator에 사용하기
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);
});

never을 이용한 잔여 케이스 체크 (exhaustiveness checking with never)

우리가 if else나 switch를 사용하다보면 모든 case를 소화했나를 확인하고 싶을 때가 있다.
그럴때는 never type을 이용해보자.

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;
  }
}

never type에는 어떠한 값도 들어와선 안된다.
정확히는 어떠한 값이 들어올만한 상황이면 안 된다.
한마디로 '대입되면 터지는 지뢰'에 가깝다.


https://www.typescriptlang.org/docs/handbook/2/narrowing.html

 

Documentation - Narrowing

Understand how TypeScript uses JavaScript knowledge to reduce the amount of type syntax in your projects.

www.typescriptlang.org