TypeScript - 집합 관계인 타입들

Minkyu Shin·2023년 6월 15일
1

TypeScript

목록 보기
6/7
post-thumbnail
post-custom-banner

TypeScript

본 내용은 한 입 크기로 잘라먹는 타입스크립트 강의를 바탕으로 작성되었습니다. 사진 자료의 출처 또한 위 강의입니다.

타입 = 집합

타입스크립트의 타입은 여러 종류의 값을 요소로 가진 집합과도 같다. 예를 들어 number 타입은 정수, 소수, Infinity 등 다양한 숫자 값들을 묶어 놓은 집합이라고 볼 수 있다.

number 리터럴 타입의 경우는 딱 하나의 숫자 값만 포함하는 집합이라고 볼 수 있다. 20이라는 number 리터럴 타입을 집합으로 표현한다면 다음 그림과 같을 것이다.

여기서 생각해볼 수 있는 것은 20은 결국 숫자 값이기 때문에 number 타입의 부분집합으로도 볼 수 있다는 것이다. 즉 모든 number 리터럴 타입은 number 타입의 부분집합이 될 것이다.

타입스크립트에서 다른 타입을 포함하는 더 큰 타입을 슈퍼 타입이라 하고 포함이 되는 더 작은 타입을 서브 타입이라고 한다. 이렇게 타입들은 서로 포함하고 포함되는 관계를 갖고 계층을 이루게 된다.

앞서서 기본 타입을 살펴볼 때 위 타입 계층도를 첨부한 바 있다. 그 계층도가 이 슈퍼-서브(부모-자식) 관계를 바탕으로 그려진 것이라 보면 된다.

타입 호환성

타입 호환성이란 하나의 타입을 다른 타입으로 취급할 수 있는지(즉, 호환이 되는지) 판단하는 것을 말한다. 다른 타입으로 취급이 가능하다면 '호환이 된다' 라고 표현하고 그렇지 않으면 '호환이 되지 않는다' 라고 표현한다.

number 타입과 number 리터럴 타입의 호환성을 살펴보자. number 리터럴 타입은 number 타입과 호환이 된다고 말할 수 있다. 하지만 그 반대는 호환되지 않는다. number 타입이number 리터럴 타입에 속하지 않는 다양한 값들을 포함하는 더 큰 집합이기 때문이다.

쉽게 부분집합의 개념을 생각해보면 좋다. 집합 A의 부분집합 B가 있다고 가정해 보자. 집합 B의 모든 원소들은 집합 A에 속한다. 따라서 호환이 된다고 할 수 있다. 하지만 집합 A의 모든 원소들이 집합 B에 속한다고 할 수는 없다. 집합 B가 더 작은 집합일 수 있기 때문이다. 호환이 되지 않는 것이다.

타입스크립트에서는 서브타입의 값을 슈퍼타입의 값으로 취급하는 것을 허용하며 이를 업 캐스팅이라고 부른다. 그 역은 특별한 경우가 아니고서는 허용하지 않고 다운 캐스팅이라고 부른다.

타입 계층도로 살펴보는 타입 호환성

다시 타입 계층도를 불러와서 타입 간의 관계를 살펴보자. 계층도 상 위쪽에 위치한 타입을 슈퍼타입, 아래쪽에 위치한 타입을 서브타입으로 볼 수 있다.

unknown 타입 (전체 집합)

unknown 타입은 계층도의 최상단에 위치하고 있다. 모든 타입의 슈퍼타입으로써 모든 타입을 unknown 타입으로 업 캐스팅 할 수 있다.

let a: unknown = 1;                 // number -> unknown
let b: unknown = "hello";           // string -> unknown
let c: unknown = true;              // boolean -> unknown
let d: unknown = null;              // null -> unknown
let e: unknown = undefined;         // undefined -> unknown
let f: unknown = [];                // Array -> unknown
let g: unknown = {};                // Object -> unknown
let h: unknown = () => {};          // Function -> unknown

집합 구조를 그림으로 나타내면 다음과 같다. 타입스크립트에서 unknown 타입은 모든 타입들을 부분집합으로 갖는 전체 집합이다.

한가지 알아둬야 할 점은 unknown 타입에 다운캐스팅 예외가 있다는 것인데, unknown 타입을 any 타입으로 다운캐스팅은 예외적으로 가능하다. 다른 타입들은 예외 없이 다운캐스팅이 불가하다.

let a: any;
let b: number;
let c: unknown;

a = c; // 예외적으로 가능하다.
b = c; // Error

never 타입 (공집합)

never 타입은 계층도의 최하단에 위치하고 있다. never 타입은 불가능을 의미하는 타입으로 그 어떤 값도 할당 할 수 없다. 집합의 관점에서 보면 공집합인 것이다.

공집합은 모든 집합의 부분집합이다. 따라서 never 타입은 모든 타입의 서브타입이다. never 타입을 모든 타입으로 업캐스팅 할 수 있다.

let neverVar: never;

let a: number = neverVar;            // never -> number
let b: string = neverVar;            // never -> string
let c: boolean = neverVar;           // never -> boolean
let d: null = neverVar;              // never -> null
let e: undefined = neverVar;         // never -> undefined
let f: [] = neverVar;                // never -> Array
let g: {} = neverVar;                // never -> Object

하지만 다운캐스팅은 절대 불가능하다.

any 타입 (특별함 ⭐️⭐️⭐️)

any 타입은 타입 계층도를 무시하는 특별한 타입이다. 모든 타입의 슈퍼타입이자 서브타입이 될 수 있다. 즉, 모든 타입이 업캐스팅 될 수 있고 모든 타입으로 다운캐스팅도 될 수 있다.

let anyValue: any;

let num: number = anyValue;   // any -> number (다운 캐스트)
let str: string = anyValue;   // any -> string (다운 캐스트)
let bool: boolean = anyValue; // any -> boolean (다운 캐스트)

anyValue = num;  // number -> any (업 캐스트)
anyValue = str;  // string -> any (업 캐스트)
anyValue = bool; // boolean -> any (업 캐스트)

객체 타입의 호환성

앞서 기본 타입간의 호환성을 알아보았다. 그렇다면 객체 타입의 호환성은 어떻게 결정될까?
객체 타입간의 호환성도 동일한 판단 기준을 가지고 있다. 모든 객체 타입은 다른 객체 타입들과 슈퍼-서브 타입 관계를 가지고 업캐스팅이 허용, 다운캐스팅이 허용되지 않는다.

객체 타입에서 슈퍼-서브 타입을 결정 짓는 것은 프로퍼티 이다. 타입스크립트는 프로퍼티를 기준으로 타입을 정의하는 구조적 타입 시스템을 따른다.

다음 예시를 통해 구체적으로 알아보자.

type Animal = {
  name: string;
  color: string;
};

type Dog = {
  name: string;
  color: string;
  breed: string;
};

let animal: Animal = {
  name: "기린",
  color: "yellow",
};

let dog: Dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도",
};

animal = dog; // ✅
dog = animal; // ❌

Animal 타입은 Dog 타입의 슈퍼타입이다. 혹자는 프로퍼티가 하나 더 많은 Dog 타입이 슈퍼타입일 것이라 생각할 수 있지만 생각해보면 그렇지 않다.
다시 집합의 관점에서 생각해보자. Animal 타입은 객체들 중 name , color 프로퍼티들을 가진 모든 객체를 포함하는 집합이다. Dog 타입은 객체들 중 name , color , breed 프로퍼티들을 가진 모든 객체를 포함하는 집합이다. Dog 타입의 모든 객체들은 Animal 타입에 포함될 수 있지만, 그 역은 성립되지 않는다. 따라서 Animal 타입이 슈퍼타입이 된다.

초과 프로퍼티 검사

초과 프로퍼티 검사는 타입에 정의된 프로퍼티 외 초과된 프로퍼티를 갖는 객체를 변수에 할당할 수 없도록 막는 기능이다. 이 기능은 변수를 객체 리터럴로 초기화 할 때만 발동한다.

type Book = {
  name: string;
  price: number;
};

let book: Book = {
  name: "example",
  price: 20000,
  category: "tech", // Error
};

let someBook = {
  name: "example",
  price: 20000,
  category: "tech",
};

let book2: Book = someBook; // 객체 리터럴이 아니므로 오류 발생 X

초과 프로퍼티 검사는 함수의 매개변수에도 동일하게 발생한다. 매개변수로 객체를 전달할 때 매개변수의 타입보다 프로퍼티가 많다면 오류가 발생하게 된다. 이 때에도 변수에 값을 담아 인수로 전달하면 검사를 피할 수 있다.

대수 타입 (Algebraic Type)

대수 타입은 여러개의 타입을 합성해서 만드는 타입을 말한다. Union(합집합) 타입, Intersection(교집합) 타입이 존재한다.

Union(합집합) 타입

유니온 타입은 바 | 를 이용하여 정의한다.

// Union Type
let a: string | number;

위와 같이 타입을 정의하면 변수 a 에 number 타입과 string 타입 값을 모두 저장할 수 있게 된다. 유니온 타입으로 연결할 수 있는 타입의 개수에 제한은 없다. 바를 이용하여 계속 이어주면 여러개의 타입을 유니온 타입으로 구성할 수 있게 된다.

유니온 타입을 활용하면 여러 타입의 요소를 저장하는 배열도 쉽게 표현할 수 있다.

let arr: (string | number | boolean)[] = ["hi", 10, true];

Union 타입과 객체 타입

물론 객체 타입의 유니온 타입도 정의할 수 있다.

type Dog = {
  name: string;
  color: string;
};

type Person = {
  name: string;
  language: string;
};

type Union = Dog | Person

만들어진 유니온 타입을 그림으로 나타낸다면 다음과 같을 것이다.

따라서 유니온 타입에 어떤 객체가 포함될 수 있을지 본다면 다음 코드와 같을 것이다.

let ex1: Union = {
  name: "",
  color: "",
}; // 가능 ✅

let ex2: Union = {
  name: "",
  language: "",
}; // 가능 ✅

let ex3: Union = {
  name: "",
  color: "",
  language: "",
}; // 가능 ✅

let ex4: Union = {
  name: "",
}; // 불가능 ❌

변수 ex4 의 경우 Person 타입에도 Dog 타입에도, 그 교집합에도 포함되지 않는 객체이기 때문에 포함될 수 없는 것이다.

Intersection(교집합) 타입

인터섹션 타입은 & 를 이용하여 정의한다. 교집합 타입이므로, 나열된 타입 모두를 만족하는 타입 값만을 허용한다.

let a: string & number;
// never 타입

위와 같이 기본 타입끼리의 인터섹션 타입도 정의할 수 있다. 하지만 두 타입은 교집합이 없는 서로소 집합이기 때문에 결국 never 타입이 된다.

대부분의 기본 타입들 사이에는 교집합이 존재하지 않기 때문에 인터섹션 타입은 보통 객체 타입들에서 자주 사용된다.

Intersection 타입과 객체 타입

객체 타입의 인터섹션 타입도 동일하게 & 를 사용하여 정의하면 된다.

type Dog = {
  name: string;
  color: string;
};

type Person = {
  name: string;
  language: string;
};

type Intersection = Dog & Person

만들어진 인터섹션 타입을 그림으로 표현하면 다음과 같다.

두 타입의 '교집합' 만을 포함하는 것을 알 수 있다. 따라서 인터섹션 타입에 어떤 객체가 포함될 수 있을지 본다면 다음 코드와 같을 것이다.

let ex1: Intersection = {
  name: "",
  color: "",
  language: "",
};

서로소 Union 타입

서로소 유니온 타입은 서로소 관계에 있는 타입들을 모아 만든 유니온 타입을 말한다.

type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};

type Member = {
  tag: "MEMBER";
  name: string;
  point: number;
};

type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number;
};

function login(user: User) {
  switch (user.tag) {
    case "ADMIN": {
      console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
      break;
    }
    case "MEMBER": {
      console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
      break;
    }
    case "GUEST": {
      console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
      break;
    }
  }
}

서로소 객체 타입별로 tag 프로퍼티를 추가하여 이후 함수에서 타입을 좁힐 수 있도록 해 주었다.

profile
개발자를 지망하는 경영학도
post-custom-banner

0개의 댓글