Narrowing[Typescript]

SnowCat·2023년 2월 13일
0

Typescript - Handbook

목록 보기
4/9
post-thumbnail
  • 다음과 같은 함수를 살펴보자
function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}
  • padding값으로 string, number가 올 수 있는데, repeat 함수는 number만을 받을 수 있기 때문에 타입 검사를 우선 진행해야 한다.
  • 자바스크립트의 typeof 연산자를 활용해 타입 체크를 진행하는데, 이는 type guard라고 부름
  • 예제와 같이 type이나 할당된 값을 확인해 타입을 구체적으로 정의하는 행동을 narrowing이라고 함

typeof type guards

  • 원시 타입에 대해서는 앞선 예제코드와 같이 타입검사가 가능함
  • 하지만 null이나 배열을 처리할 때 불편함이 발생함
function printAll(strs: string | string[] | null) {
  // 1. 배열은 자바스크립트에서 객체이기 때문에 object로 처리해야함
  // 2. null도 객체이기 때문에 의도와 다르게 else문으로 가지 않음
  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
  }
}

Truthiness narrowing

  • boolen을 만들어주는 연산자들 (&&, ||, !)과 조건문을 사용해 타입 범위를 좁힐 수 있음
  • if문에서는 0, NaN, 빈 문자열, 0n(bigint 타입에서 0을 표현하는 경우) null, undefined가 값으로 입력되면 false, 나머지 경우는 true를 출력함
// 주의 - strs === "" 인 경우에 null과 같이 아무것도 하지 않음
function printAll(strs: string | string[] | null) {
  if (strs) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
    }
  }
}

// 올바른 타입 추론 방법
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);
  }
}
  • boolean을 직접 얻고자 할 경우 Boolean()을 사용하거나, 느낌표를 2번 붙이는 방식으로 값을 얻을 수 있음
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true(범위가 더 좁아짐), value: true

Equality narrowing

  • switch문이나 ===, !==, ==, != 과 같은 확인 연산자를 통해서도 타입 범위를 좁힐 수 있음
function example(x: string | number, y: string | boolean) {
  if (x === y) { //일치하는 타입은 string 뿐임으로 x, y는 string으로 처리됨
    x.toUpperCase();
    y.toLowerCase();
  } else {
    console.log(x); // string | number
    console.log(y); // string | boolean
  }
}

// printAll 함수 수정
function printAll(strs: string | string[] | null) {
  if (strs !== null) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
    }
  }
}
  • ==, !=를 사용하는 경우 null과 undefined는 동일하게 취급됨
interface Container {
  value: number | null | undefined;
}
 
function multiplyValue(container: Container, factor: number) {
  // != 연산자를 사용해 !== 연산자를 두번 쓰지 않고 예외처리 가능
  if (container.value != null) { //null과 undefined 동시에 제거
    console.log(container.value);
    container.value *= factor;
  }
}

The in operator narrowing

  • 자바스크립트의 in 연산자를 사용해 타입스크립트에서 narrowing을 할 수 있음
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
 
function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) {
    // Fish | Human
  } else {
    // Bird | Human
  }
}

instanceof narrowing

  • instanceof: 자바스크립트에서 프로토타입 체인에 특정 객체가 있는지를 확인하는 연산자
  • instanceof 역시 type guard로 사용 가능하며, 클래스나 객체에 유용하게 사용 가능함
function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString()); //Date
  } else {
    console.log(x.toUpperCase()); //string
  }
}

Assignments

  • 직접적으로 타입을 할당하지 않았을 경우 타입스크립트는 처음 주어진 할당식을 확인해 타입 범위를 결정하게 됨
let x = Math.random() < 0.5 ? 10 : "hello world!"; //string | number
   
x = 1; //number
console.log(x);
           
x = "bye" //string
console.log(x);

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

Control flow analysis

  • 변수의 특정 타입이 코드에 도달할 수 없는 경우, 타입스크립트는 코드의 타입 범위를 좁히게 됨
function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  // padding이 number인 경우는 이 부분에 도달 불가능
  return padding + input; // padding: string
}
  • 타입스크립트가 변수의 도달 가능성을 기준으로 코드를 분석하는 방법을 Control flow analysis라고 부름
  • 코드의 변수를 분석하는 과정에서 타입 범위는 좁아질수도, 넓어질수도, 분리되거나 다시 합쳐질수도 있음
function example() {
  let x: string | number | boolean;
 
  x = Math.random() < 0.5;
  console.log(x); //boolean
 
  if (Math.random() < 0.5) {
    x = "hello";
    console.log(x); //string
  } else {
    x = 100;
    console.log(x); //number
  }
  return x; //string | number
}

Using type predicates

  • 필요한 경우 is를 활용해 타입을 직접 좁힐수도 있음
function isFish(pet: Fish | Bird): pet is Fish { //pet은 Fish로 간주
  return Boolean((pet as Fish).swim);
}

let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim(); // Fish
} else {
  pet.fly(); // Bird
}

// 배열을 좁힐때에도 사용 가능
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});

Discriminated unions

  • 원과 정사각형을 나타내는 인터페이스와 넓이를 구하는 함수를 작성해보자
interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius! ** 2;
  }
  else {
    if (shape.sideLength?) return shape.sideLength? ** 2;
  }
}
  • 타입에 어떤 속성이 들어가는지 확실히 알고 있음에도 불구하고 느낌표나 불필요한 optional chaining을 사용해야 한다.
  • 타입스크립트가 구체적으로 어떤 속성이 있는지를 알게 하려면, 두개의 인터페이스를 작성하고, 이들을 union으로 묶어줘야 함
interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
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;
  }
}

The never type

  • 타입을 좁히는 과정에서 아무런 타입도 해당이 되지 않는 경우는 never 속성이 됨
  • never 타입은 never 타입이 아닌 어떠한 것도 할당할 수 없기 때문에 이를 활용해 타입이 추가될 경우 분기처리를 하지 않은 경우의 오류를 잡아낼 수 있음
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;
  }
}

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

profile
냐아아아아아아아아앙

0개의 댓글