(수근수근)

Typescript handbook 읽고 정리(Narrowing) 본문

type & Javascript

Typescript handbook 읽고 정리(Narrowing)

InformationFarm 2022. 10. 20. 00:53

Narrowing

narrowing 타입을 좁혀나간다 이런 의미같다.. 살펴보자

해당 장에서 타입스크립트가 제공하는 (자바스크립트가 제공하는 키워드이기도 하지만..? 이를 타입스크립트는 narrowing용으로 사용도 하는 것 같다.) Type guards들에 대해서 하나씩 알아보자! 

 

우선 첫 번째 예제를 하나 보여주는데 아래와 같다

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

위의 함수를 보면 일단 자바스크립트 코드처럼보인다. 이 것이 요점인데, 타입스크립트는 타입안정성을 위해 자바스크립트 코드를 최대한 쉽게 작성하는 것을 목표료 한다는 것이다.

타입스크립트는 typeof라는 타입 가드의 형태로 구체적인 유형으로 한번 더 Narrowing해주는 작업을 진행한다. 그럼 type guards인 typeof키워드에 대해서 알아보자

typeof type guards

자바스크립트 typeof를 지원하여 런타임에 우리가 가지고 있는 값의 유형에 대해 정보를 제공한다. TypeScript는 이를 통해 특정 문자열 집합을 반환할 것으로 예상한다.

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

padLeft에서 보았 듯이, typeof는 JavaScript 라이브러리에서 자주 사용하여 TypeScript는 다른 분기의 유형을 좁히는 것을 이해할 수 있습니다.

TypeScript에서 typeof에 반환되는 값에 대해 체크하는 것이 유형 가드입니다.

타입스크립트는 타입이 다른 값에서 어떻게 동작하는지 인코딩하기 때문에 자바스크립트에서 그것의 기묘함 중 일부를 알고 있다. 아래의 예를 봐보면

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

위의 코드를 보면 Object is possibly 'null' 에러를 내뿜는데, 그 이유는 object타입으로 타입을 좁히면 string[], null이 narrowing이 된다. (null의 타입도 object라는 사실~!)

Truthiness narrowing

진실은 사전에서 찾을 수 있는 단어는 아닐지 몰라도, 자바스크립트에서는 아주 많이 들을 수 있는 단어야.JavaScript에서 조건에 대한 표현식을 사용하는데, 예를 들어 &&, ||, if, ! 등등이 있다.

자바스크립트에서는 if와 같은 분기문으로 해당 조건에 따라 강제분기를 합니다.

  • 0
  • NaN
  • “” (empty string)
  • null
  • 0n(the bigint version of Zero)
  • null
  • undefined

위의 값들은 false로 강제되고, 나머지는 다 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);
  }
}

코드를 보면 자연스럽게 truthy만 확인함으로써 나머지 에러들을 자연스럽게 제거하는 것을 확인할 수 있다.

typeof 키워드만으로는 Null체크를 못해주기 때문에, 앞에서 falsy를 필터링해주어서 narrowing을 해준다.

truthiness을 boolean을 !부정함으로써 좁히는 것도 방법입니다.

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

Equality narrowing

TypeScript는 또한 switch 문을 사용하고 ==,! ==, ==, == 및! =와 같은 동등성 검사를 좁은 유형으로 사용합니다.

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

타입스크립트는 x,y가 같은 경우는  string이여야 동일함을 알고 있다. 따라서 첫번째 if문에서 타입스크립트는 x,y를 string으로 narrowing합니다. 또한 else문에서는 다양한 경우의 수가 오는 것까지 타입스크립트가 자동으로 추론합니다.

The in operator narrowing

JavaScript에는 개체에 이름이 있는 속성인 in 연산자가 있는지 여부를 결정하는 Type Guard가 있습니다. TypeScript는 잠재타입을 in으로 narrowing하여 고려합니다.

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

animal에는 Fish와 Bird가 있는데, animal속성에 따라 다르게 move동작을 해야하는데 이럴 때 in을 사용하여 narrowing합니다.

instanceof narrowing

자바스크립트에는 값이 다른 값의 "인스턴스"인지 여부를 확인하는 typeguard 연산자가 있다. 자바스크립트 x 인스턴스에서 foo의 프로토타입 체인이 foo.protype을 포함하고 있는지 확인한다.

예상하셨겠지만, instanceof 또한 타입가드이며 typeScript는 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 (할당을 통한 narrowing)

앞서 말했듯이 어떤 변수에 대입하면 TypeScript는 =할당 기준으로 오른쪽을 먼저 해석하여 왼쪽의 타입을 적절하게 좁힙니다.

아래의 코드를 보면 처음에 삼항연산자를 통해서 x를 string과 number로 자동 narrowing을 해줍니다.

 

이후에 x에 boolean값을 할당하면 x는 처음에 선언한 것만 가능하다고 타입스크립트가 에러를 던져주는 것을 확인할 수 있습니다.

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'.
//처음에 선언한 것만 사용이 가능하다.
console.log(x);
     //let x: string | number

Control flow analysis

지금까지 TypeScript가 특정 분기 내에서 좁혀지는 몇 가지 기본적인 예를 살펴보았다. 하지만 모든 변수에서 살펴보면 if, while, conditional 등을 통해 typeguard를 찾는 것 이상의 일이 있습니다.

⇒ 코드의 흐름에 컨트롤에 따라서 typeguard가 지정이 된다는 의미같다. (잘 모르겠음)

 

Using type predicates 

우리는 지금까지 좁히기를 처리하기 위해 기존 JavaScript 구문과 함께 작업했지만 때로는 코드 전체에서 유형이 어떻게 변경되는지에 대한 직접적인 제어를 원할 수도 있습니다. 사용자 정의 type guard를 직접 정의하기 위해서  is키워드를 통해 제어해줄 수 있습니다.

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

isFish가 일부 변수와 함께 호출될 때마다, TypeScript는 만약 원래 타입이 호환이 되면 해당 변수를 특정 유형으로 좁힙니다.(isFish가 type guard이다)

 

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

Discriminated unions

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

위를 보면 문자의 타입을 union을 사용하고있음을 알 수 있다.

위처럼 사용하면 스펠링 오류도 해결할 수 있다.

넓이를 구하기위해서 아래와 같이 사용하면 에러가 난다.

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

반지름이 정의되지 않을 수도 있기 때문에 에러가 난다. shape의 속성에 검사를 수행한다면?

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

radius가 옵셔널이기 때문에 없을 수도 있다고 에러가난다. 우리는 위에서 배운 ! 을 사용하여 무조건 undefined가 아니라고 말한다.

function getArea(shape: Shape) {
  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;

→ Shape를 분리하여 필요한 속성에 따라 다르게 분리한다.

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

이는 오류를 없앴다! 유니언의 모든 타입이 리터럴 타입을 가진 공통 속성을 가지고 있을 때, 타입스크립트는 그것을 구별되는 유니언으로 간주하고 유니언의 구성원을 좁힐 수 있다.

Discriminated unions 는 이러한 예제 뿐만아니라, 네트워크(클라이언트/서버 통신)를 통해 메시지를 전송하거나 상태 관리 프레임워크에서 돌연변이를 인코딩할 때처럼 자바스크립트에서 모든 종류의 메시징 체계를 표현하는 데 유용하다.

The never Type

범위를 좁힐 때 조합의 선택권을 모든 가능성을 제거하고 남는 것이 없을 정도로 줄일 수 있습니다. 이 경우 TypeScript는 존재하지 않아야 하는 상태를 나타내기 위해 never 유형을 사용합니다.

Exhaustiveness checking (철저한 체크!)

Never 유형은 모든 유형에 할당할 수 있지만, Never 유형은 할당할 수 없다(Never 자체는 제외). 이것은 당신이 좁히기를 사용할 수 있고 스위치 문에서 철저한 점검을 하기 위해 절대 나타나지 않는 것에 의존할 수 있다는 것을 의미한다. 예를 들어, 모든 가능한 경우가 처리되지 않았을 때 절대로 발생하지 않을 모양을 할당하려고하는 getArea 함수에 기본값을 추가하십시오.

 

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 타입에 Triangle을 추가하게 되면 타입스크립트는 어떠한 타입도 와서는 안되는 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;
  }
}
Comments