집합으로 배우는 타입 호환성

blueprint·2023년 11월 8일
0
post-thumbnail

✏️ 타입 호환성이란?

타입스크립트에서 특정 타입의 값을 다른 타입의 값으로 취급해도 괜찮은지 판단하는 기준

number - number 리터럴 타입의 값이 있을 때 이 타입들은 서로 호환이 될까?
그렇지 않다. number는 number 리터럴 타입 값이 될 수 있지만, number 리터럴 타입은 number 타입이 될 수 없다. 타입스크립트는 업 캐스팅은 가능하지만, 다운 캐스팅은 불가능하다고 판단한다.

  • 업 캐스팅: 서브 타입을 슈퍼 타입의 값으로 취급
  • 다운 캐스팅: 슈퍼 타입을 서브 타입의 값으로 취급

아래의 예를 참고했을 때 모든 강아지는 동물이 될 수 있지만(업 캐스팅), 모든 동물은 강아지가 될 수 없는 것과 같다.(다운 캐스팅)

  • 모든 강아지 = 동물
  • 모든 동물 ≠ 동물

또 다른 예시를 보자. a는 number 타입이고, b는 number 리터럴 타입이다. a가 b의 슈퍼 타입이 된다. 고로 업 캐스팅인 a = b는 가능하지만, 다운 캐스팅인 b = a는 불가능하다.

let a: number = 12;
let b: 30 = 30;

a = b; // 호환 O, 업 캐스팅
b = a; // 호환 X, 다운 캐스팅

✏️ 특수 타입 (Unknown, Never, Any)

1. Unknown

unknown은 최상단에 위치하므로 모든 타입의 슈퍼 타입이라고 할 수 있다. 그렇기 때문에 모든 타입의 값을 저장할 수 있다.

  • 업 캐스팅 (호환 O)
let a: unknown = 123;
let b: unknown = "hello world";
let c: unknown = true;
let d: unknown = null;
let e: unknown = undefined;
let f: unknown = [];
let g: unknown = {};
let h: unknown = () => {};
  • 다운 캐스팅 (호환 X)
let unknownVar: unknown;

let num: number = unknownVar;
let str: string = unknownVar;
let bool: boolean = unknownVar;

unknown은 언제 사용해야 할까?

  1. 현재 정확한 타입을 알기 어려울 때
  2. 타입 좁히기와 함께 유연하게 사용

foo라는 함수에서는 매개 변수 value를 unknown 타입으로 받고 있다. unknown은 최상위에 위치하고 있기 때문에 어떠한 타입이라도 다 들어올 수 있게 된다. 그렇지만 안의 조건문을 통해서 타입 좁히기를 할 수 있다. value가 number인 경우에 실행할 로직, string일 경우에 실행할 로직을 따로 작성하면 특정 타입으로 좁혀 원하는 동작을 실행시킬 수 있다.

function foo(value: unknown) {
  if (typeof value === "number") {
    return value.toFixed();
  } else if (typeof value === "string") {
    return value.toLocaleLowerCase();
  } else {
    return value;
  }
}

foo(1);
foo("hello");
foo(undefined);

2. Never

Never는 unknown과 정반대의 특징을 가진다. 최하위 계층에 위치하기 때문에 모든 타입의 서브 타입이다.

  • 업 캐스팅 (호환 O)
let neverVar: never;

let num: number = neverVar;
let str: string = neverVar;
let bool: boolean = neverVar;
  • 최하위 계층이기 때문에 다운 캐스팅이 이루어질 수 없음 (호환 X)
let a: never = 123;
let b: never = "hello world";
let c: never = true;
let d: never = null;
let e: never = undefined;
let f: never = [];
let g: never = {};
let h: never = () => {};

never는 언제 사용해야 할까?

  1. 호출되지 않아야 하는 함수를 만들 때
  • error라는 함수에서 받고 있는 매개 변수 value는 never 타입을 가지기 때문에 어떠한 값도 넣을 수 없게 된다.
function error(value: never) {
  throw new Error();
}

error(); // 인수 없음, 오류
error(1); // 불가
error("hello"); // 불가
error(undefined); // 불가
  1. Switch의 완전성을 보장하기 위해
  • case가 "BLUE"일 때 대한 처리는 작성하지 않음 → 완전성이 보장되지 않은 Switch문
  • 완전하지 않은 Switch는 의도치 않은 동작을 발생시킴
  • default문을 추가하고 error 함수를 호출했을 때 완전성이 보장되지 않았음을 알 수 있다.
function error(value: never) {
  throw new Error();
}

type Color = "RED" | "GREEN" | "BLUE"

function getColorName(color: Color) {
  switch(color) {
    case "RED":
      return "rgb(255, 0, 0)";
    case "GREEN":
      return "rgb(0, 255, 0)";
    default: {
      return error(color);
    }
  }
}

3. Any

any는 타입 검사를 받지 않는다. unknown 타입처럼 모든 타입의 값을 다 저장할 수 있고, never처럼 어느 타입의 변수든 저장될 수 있기도 하다. 모든 타입의 슈퍼 타입이 되기도 하고, 서브 타입이 되기도 한다. 그래서 any는 치트키 역할이라고 할 수 있다. 그러나 딱 한 가지 예외가 있다. never 타입에는 any를 넣을 수 없다. never는 공집합이기 때문에 어떤 값도 포함할 수 없기 때문이다.

let anyVar: any;

// 어느 타입이든 저장할 수 있음
anyVar = 2023;
anyVar = "hello world!";
anyVar = true;
anyVar = undefined;
anyVar = null;

// 어느 타입이든 저장될 수 있음
let num: number = anyVar;
let str: string = anyVar;
let bool: boolean = anyVar;
let _null: null = anyVar;
let _unde: undefined = anyVar;

any는 언제 사용해야 할까?

  1. 불가능한 타입 단언을 가능하게 할 수 있음
  • str1: number → string으로의 단언은 불가능하다.

    • 타입 단언은 단언 전의 타입과 단언 이후의 타입이 슈퍼 - 서브 타입 관계를 가져야 한다.
    • number와 string은 겹치는 구석이 없음.
  • str2: number → any → string의 형식으로 단언은 가능하다.

    • number를 any로 단언, any를 string으로 단언
    • any는 어떤 타입이든 저장할 수 있고, 저장될 수 있으므로 가능하다.
  • str3: number → unknown → string

    • number를 unknown으로 단언, unknown을 string으로 단언
    • unknown 역시 모든 타입의 슈퍼 타입이 되기 때문에 가능하다.
let str1: string = 10 as string; // 오류
let str2: string = 10 as any as string; // 실행
let str3: string = 10 as unknown as string; // 실행

🔗 참고
인프콘 2023 다시보기
타입 단언 | 타입스크립트 핸드북
한 입 크기로 잘라먹는 타입스크립트 - 타입 계층도와 함께 기본 타입 살펴보기

0개의 댓글