TypeScript - Narrowing

CH_Hwang·2022년 3월 30일
0

TypeScript

목록 보기
10/12

Narrowing

다음과 같은 함수가 있다

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

padding이 number면 input앞에 공백을 갯수만큼 붙일 것이고 string이면 그냥 input앞에 붙이고 싶다면 아래와 같이 수정한다.

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

타입스크립트가 자바스크립트에서 type을 안전하게 사용하려고 만든 타입시스템의 목적을 기억해라.

if문 안에서 padding의 타입은 number로 특정지어진다.
그리고 if문 밖에서는 string으로 특정지어진다.

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

typeof type guards

typeof 연산자는 null을 반환하지 않는다.

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

자바스크립트에서는 array타입도 object 타입이기 때문에 strs는 object타입이다. 또한 typeof null의 결과값 역시 "object"이다.

Truthiness narrowing

자바스크립트에서는 어떠한 조건연산자도 사용할 수 있다. if구문에서는 !를 포함해서.
또한 if문은 조건을 강제로 boolean으로 만든다.

  • 0
  • NaN
  • "" (빈 문자열)
  • 0n (bigint 버전 0)
  • null
  • undefined

위의 경우 모두 false로 강제되고 나머지 값들은 모두 true로 강제된다.
이를 확인하고 싶으면 Boolean 함수를 쓰거나 !!를 쓰면 편하다.

Boolean("hello"); //type:boolean value:true
!!"world"; // type:true value: true

우리는 위의 오류가 났던 코드를 아래와 같이 변경할 수 있다.

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

Equality narrowing

타입스크립트는 switch문과 ===,!==,==,!= 같은 동등연산자 또한 타입을 좁히기 위해 사용할 수 있다.

function example(x: string | number, y: string | boolean){
  if(x === y) {
    console.log(typeof x) // string
    console.log(typeof y) // string
    x.toUpperCase();
    y.toUpperCase();
  } esle {
    console.log(typeof x); // string | number
    console.log(typoef y); // string | boolean
  }
}

x === y타입까지 똑같은걸 검사하는 연산자이기 때문에 타입이 특정 지어진다.

in operator narrowing

in 연산자는 자바스크립트 연산자로, 객체가 해당 이름을 가진 property가 있는지 검사해준다.

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

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

instanceof narrowing

instanceof 연산자는 다른 값의 instance에 해당 값이 있는지 없는지를 판단해준다.
x instanceof Foox가 Foo.prototype에 포함되는지 체크한다.
해당 연산자는 class에서 new와 함께 생성되었을때 더 유용하다.

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString()); // x:Date
  } else{
    console.log(x.toUpperCase()); // x:string
  } 
}
// 생성자 정의
function C(){}
function D(){}
 
var o = new C();
 
// true, 왜냐하면 Object.getPrototypeOf(o) === C.prototype
o instanceof C;
 
// false, 왜냐하면 D.prototype이 o 객체의 프로토타입 체인에 없음
o instanceof D;

Assignments

타입스크립트는 변수를 할당 할 때 오른쪽을 보고 적절하게 type narrowing을 해줌

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

x = 1; // x: number

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

하지만 전혀다른 새로운 타입으로는 할당이 되지 않음

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

x = 1; // x: number

x = true; // Type 'boolean' is not assignable to type 'string | number'.

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

Using type predicates

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

pet is Fishtype predicates이다. 예측은 parameterName is Type 형태로 구성된다. parameterName은 반드시 현재 함수의 파라미터 이름이여야한다.

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

Discriminated unions

우리가 보았떤 예시들은 string, boolean, number같은 단일 변수에 대한 narrowing이었다. 여기서는 조금 더 복잡한 구조를 시도해보겠다.

interface Shape {
  kind: "circle" | "square";
  radius?: number; // 원에 대한 특성
  sideLength?: number; // 사각형에 대한 특성
}

circleradius를 가질 것이고 squaresideLength를 가질 것 이다.
kind에 string을 쓰는 대신 "circle" | "square" 와 같은 문자열 리터럴을 쓰면 스펠링오류같은 걸 방지할 수 있다.

function handleShape(shape: Shape) {
  // oops!
  if (shape.kind === "rect") { //This condition will always return 'false' since the types '"circle" | "square"' and '"rect"' have no overlap.
    // ...
  }
}
function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2; //Object is possibly 'undefined'.
}

radius는 optional이기때문에 number | undefined이다. 여기서 kind를 지정해준다고 한들 여전히 같은 오류일 테지만 우리는 non-null assertion을 사용할 수 있다.

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

이 방법은 이상적이지 않다. 이럴때는 차라리 다음과 같이 바꾸는 것이 낫다.

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2; // shape: Circle
  }
}

Exhaustiveness checking

never 타입은 어떤 타입에도 할당 가능하지만 어떤 타입도 never타입에 할당할 수 없다.

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 union에 추가를 하면 에러가 난다.

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'.
      return _exhaustiveCheck;
  }
}

이럴 땐 case에 추가해주면 나지 않는다.

3개의 댓글

comment-user-thumbnail
2022년 3월 30일

잘 읽고 갑니다 :)

답글 달기
comment-user-thumbnail
2022년 4월 1일

잘 읽고 갑니다~~

답글 달기
comment-user-thumbnail
2022년 4월 21일

이 좋은 글을 이제야 봤네요.. 역시 갓갓창창황.....

답글 달기