Narrowing

  • 타입 좁히기는 TS가 코드 실행 중, 로직 상에서 가능한 타입을 줄여 나가는 과정을 의미한다.
function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}
  • 위의 예시를 보면 typeof 를 활용하여 타입을 좁혀 나가고, 그 외의 경우인 경우를 역시 추론하여 padding 이 string일 수 있다고 생각, return 문을 문제없이 넘어가게 만든다.
  • TS는 이러한 기능을 통해 코드 타입의 안정성을 보장해주고, 이러한 방식으로 조건문, 반복문, 진리성 체크, 삼항 식 등을 통해 코드를 명확하고 오류가 없게 만드는 것이다.

typeof type guards

  • JS에서 지원하는 typeof 연산자를 TS에서도 동일하게 지원한다.
    • string
    • number
    • bigint
    • symbol
    • undefined
    • object
    • funtion
  • 이러한 기능을 활용하여 연산자를 통해 타입을 축소하는 것을 타입 가드라고 부른다.
function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

Truthiness narrowing

  • 진실성은 사전에 정의된 명칭은 아니지만, JS에서도 TS에서도 사용되는 형태이다.
function getUsersOnlineMessage(numUsersOnline: number) {
  if (numUsersOnline) {
    return `There are ${numUsersOnline} online now!`;
  }
  return "Nobody's here. :(";
}
  • JS에서는 조건을 boolean으로 강제 변환하여 true, false 분기를 진행하는데 이때 0, NaN, ""(빈문자열), 0n, 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);
  }
}
  • 이 예시를 보면 str 가 진리성을 체크하면서 객체인 경우, string 인 경우를 모두 점검하며 null 일 가능성이 사라져 에러를 방지할 수 있다.
  • 한 가지 기억할 것은 프리미티브에 대한 진실성 검사는 오류가 발생하기 쉽다는 점이다.
  • 또한 논리 부정 연산자(!) 는 부정된 브랜치에서 값을 필터링한다.

Equality narrowing

  • TS에서는 당연히 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();
    // 모두 string 으로 축소 
  } else {
    console.log(x);
    // 이 경우는 x 는 string | number 타입으로 축소한다. 
    console.log(y);
    // 이 경우는 y 는 string | boolean 타입으로 축소한다. 
  }
}
  • TS는 위의 예시 같은 경우 같은 경우는 타입도 같다고 판단하고 이에 대해 동일하게 타입을 지정해준다.
  • 구체적인 리터럴 값에 대한 점검 역시 잘 동작한다. 이 예시에선 null 에 대해 먼저 점검하는 식으로 빈 문자열에 대한 대응도 했으며, TS는 여전히 필요한 상황에 맞춰 타입 축소를 진행한다.
function printAll(strs: string | string[] | null) {
  if (strs !== null) {
    if (typeof strs === "object") {
      for (const s of strs) {
                       //(parameter) strs: string[]
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
                   //(parameter) strs: string
    }
  }
}
  • JS의 느슨한 동등 점검인 ==, != 역시 정상적으로 타입 축소가 가능하다.
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;
  }
}

The in operator narrowing

  • JS 에는 객체 또는 객체의 프로토타입 체인에 이름이 있는 속성이 있는지 확인하는 in 연산자가 있고, 이 역시 TS에서 타입을 축소하는 도구로 사용될 수 있다.
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 narrowing

  • JS 에서 가진 연산자로 특정 객체의 인스턴스 값인지를 확인하는 연산자를 활용하여 프로토타입 체인을 확인하고, 타입의 축소를 수행할 수 있다.
  • 단 여기에 들어가야 하는 객체는 new 키워드를 통해 생성되어야 한다는 점을 유의하자.
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

  • 값을 할당 할시 TS는 오른편의 할당을 보고 값의 타입을 적절하게 추론해낸다.
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
  • 단, 최초 선언 시의 타입에 해당하지 않는 값이 할당 될 때는 에러가 발생할 수 있다.
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

  • 도달 가능성을 기반으로 코드를 분석하는 방식을 제어 흐름 분석 이라고 부르며 TS에서는 이를 활용하여 타입의 축소를 해준다.
  • 변수를 분석을 할 때, 제어 흐름을 통해 각 위치마다 다른 대상임을 판단할 수 있다.
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

  • 당신의 코드 상에서 어떤 타입으로 변한는지 더 직접적으로 제어를 원할 때가 있을 것이다. 사용자 정의에 의한 타입 가드를 정의하기 위해, 개발자는 간단하게 타입 예언이라고 불리는 리턴타입의 함수를 정의하기만 하면 된다.
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}
  • pet is Fish 는 우리의 타입을 설정해주는 역할을 한다.
  • 어떤 경우라도 isFish 가 호출이 되면, TS는 원래 유형이 호환되는 경우 해당 변수를 특정 유형으로 좁혀준다.
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}
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);
});

Assertion functions

Discriminated unions

  • 좀더 복잡한 타입들을 어떻게 축소할수 있는가. 이러한 경우를 위해 예시를 들어보면 다음과 같다.
interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}
  • 위 예제는 string 리터럴 타입의 “circle”, “square” 라는 걸로 shape의 타입을 세세하게 구분짓는다. 이러한 형태를 기준으로는 오타로 인한 이슈에 대해선 해결이 가능하다.
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.
    // ...
  }
}
  • 이제 getArea 라는 함수를 적용해볼 수 있을 것이다. 하지만 이러면서 나타나는 문제가 다음과 같이 나타난다.
function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
//'shape.radius' is possibly 'undefined'. 
}
  • strictNullChecks 옵션 하에 에러가 발생하게 되고, 그렇기에 우리는 kind 프로퍼티를 활용하여 타입 축소를 해보려고 하지만, 이러한 노력에도 똑같은 에러가 그대로 발생한다.
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
// 'shape.radius' is possibly '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의 구조를 수정한다. 그러면 getArea 는 다음과 같이 에러메시지를 보여준다.
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'.
}
  • 여전히 에러는 발생한다. 이는 내부의 구조 상 TS가 인식하기에 Shape 라는 유니온에서, Square를 먼저 확인하기 때문이다. 또한 그 안에 strictNullChecker 가 설정되어 있기 때문에 이에 대한 에러가 발생하는 것이다.
  • 그러나 이제 union 상태에서 어떤 인터페이스인지를 검증하는 구조를 한다면, TS는 union 에서 어떤 inteface인지를 축소해낼 수 있다
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
                      // (parameter) shape: Circle
  }
}
  • 이번에는 switch 문을 활용해서도 가능하며, 결론적으로 ! 라는 non-null assertion 을 쓰지 않고도 명확하게 타입 추론을 가능케 만들 수 있다.
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
  }
}
  • 결론적으로 Discriminated union 은 상당히 중요한 기술이며, JS에서 모든 종류의 메시징 체계를 나타내는데 좋은 역할을 할 수 있다.

The never type

  • 범위를 좁힐 때 모든 가능성을 제거하고 아무것도 남지 않을 정도로 Union의 옵션을 줄일 수 있다. 이러한 경우 TypeScript는 존재해서는 안 되는 상태를 나타내기 위해 never 유형을 사용한다.

Exhaustiveness checking

  • never 타입은 모든 타입에서 할당이 가능하다. 하지만 어느 타입에도 never 는 할당할 수 없다(never 타입 자신에겐 가능하다). 이러한 특성으로 개발자는 never 에 의지하여 switch 문에서 철저한 타입 점검을 하고 타입 축소를 구현할 수 있다.
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을 추가하면 다음과 같이 에러를 발생 시킬 수 있다.
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;
  }
}