💡 타입은 집합이다
타입스크립트의 타입은 사실 여러개의 값을 포함하는 집합입니다. 집합은 동일한 속성을 갖는 여러개의 요소들을 하나의 그룹으로 묶은 단위를 의미하는데 좌측 이미지는 number 타입입니다. 그러면 우측 이미지는 오직 하나의 숫자값만 포함하는 타입인 Number Literal 타입으로 아주 작은 집합이라고 볼 수 있습니다.
그리고 이 20이라는 타입에 속하는 요소인 숫자 20은 사실 number literal Type 이라는 집합 외에도 Number 타입이라는 거대한 집합에 속하는 값입니다. 따라서 모든 Number Literal 타입은 Number 타입이라는 거대한 집합에 속하는 부분 집합이라고 할 수 있습니다. 타입스크립트의 모든 타입들은 집합으로써 서로 포함하고 또 포함되는 관계를 갖습니다. 이러한 관계에서 Number 타입처럼 다른 타입을 포함하는 타입을 슈퍼 타입(부모 타입)이라고 부르고 포함되는 타입을 서브 타입(자식 타입)이라고 부릅니다.
타입 계층도를 자세히 보면 타입스크립트가 제공하는 기본 타입들 간의 집합으로써의 부모-자식 관계임을 알 수 있습니다.
💡 기본 타입의 호환성
타입 호환성이란, A와 B 타입이 존재할 때 A타입의 값을 B타입으로 취급해도 괜찮은지 판단하는 것을 의미합니다. 다음 그림과 같이 Number 타입과 Number Literal 타입이 있을 때 서브 타입인 Number Literal 타입의 값을 슈퍼 타입인 Number 타입의 값으로 취급하는 것은 가능합니다. 하지만 반대로는 불가능하며 Number 타ㄷ입이 더 큰 타입이기 때문입니다. 마치 더 큰 타입인 직사각형이 작은 타입인 정사각형이 된다? 말이 안되는 것과 같은 원리입니다.
// 기본 타입간의 호환성
let num1: number = 10; // number 타입
let num2: 10 = 10; //number리터럴 타입
num1 = num2; // ⭕️
num2 = num1; // ❌
Number 타입의 변수 num1을 선언하고 10을 할당합니다. 그리고 10(Number Literal) 타입의 변수 num2를 선언하고 값으로 10을 할당합니다. 이때 변수 num1의 타입이 더 큰 타입이기 때문에 num1에 num2를 저장하는 것은 가능합니다. 하지만 그 반대로는 불가능합니다. 변수 num1은 Number 타입으로 10 외에도 다양한 숫자 값을 담을 수 있지만 num2에는 10 외의 다른 값은 절대 담을 수 없습니다. 그러므로 num2에 num1의 값을 저장하게 되면 문제가 발생할 가능성이 크기 때문에 슈퍼타입의 값을 서브타입의 값으로 취급하는 것은 허용되지 않습니다.
그리고 서브 타입의 값을 슈퍼 타입의 값으로 취급하는 것을 업 캐스팅이라고 부르고, 반대는 다운 캐스팅이라고 부릅니다. 결론적으로 업캐스팅은 모든 상황에 가능하지만 다운 캐스팅은 거의 불가능하다고 생각하면 됩니다.
💡 객체 타입의 호환성
객체 타입 간의 호환성도 동일한 기준으로 판단합니다. 모든 객체 타입은 각각 다른 객체 타입들과 슈퍼-서브 타입 관계를 가지며, 업 캐스팅은 허용하고 다운 캐스팅은 허용하지 않는 것이죠.
type Animal = {
name: string;
color: string;
};
type Dog = {
name: string;
color: string;
breed: string;
};
let animal: Animal = {
name: "호랑이",
color: "orange",
};
let dog: Dog = {
name: "별이",
color: "brown",
breed: "시츄",
};
animal = dog; // ⭕️
dog = animal; // ❌
Animal 타입의 변수 animal에 Dog 타입의 변수 dog를 할당하는 것은 가능하며, 이는 Animal 타입이 Dog 타입의 슈퍼타입이기 때문입니다. 그런데 Dog 타입이 더 많은 프로퍼티를 정의하고 있어서 슈퍼타입처럼 보이는데...
왜 프로퍼티가 더 적은 Animal 타입이 슈퍼타입일까요?
Animal 타입은 name과 color 프로퍼티를 갖는 모든 객체를 포함하는 집합이고, Dog 타입은 name과 color 그리고 추가적으로 breed 프로퍼티를 갖는 모든 객체를 포함하는 집합입니다. 그러면 어떤 객체가 Dog 타입에 포함된다면 당연하게 Animal 타입에도 포함됩니다. 하지만 반대의 개념은 성립되지 않습니다. 그러므로 Animal은 Dog의 슈퍼타입인 셈이죠.
type Book = {
name: string;
price: number;
};
type ProgrammingBook = {
name: string;
price: number;
skill: string;
};
let book: Book;
let programmingBook: ProgrammingBook = {
name: "한 입 크기로 잘라먹는 리액트",
price: 33000,
skill: "react.js",
};
book = programmingBook; // ⭕️
programmingBook = book; // ❌
같은 이유로 Book 타입은 ProgrammingBook 타입의 슈퍼타입입니다.
• 초과 프로퍼티 검사
초과 프로퍼티 검사는 객체 타입에 정의된 프로퍼티만 담을 수 있도록 하는 검사입니다.
type Book = {
name: string;
price: number;
};
type ProgrammingBook = {
name: string;
price: number;
skill: string;
};
let book2: Book = {
name: "한 입 크기로 잘라먹는 리액트",
price: 33000,
skill: "react.js", // ❌
};
이와 같이 Book타입의 book2라는 변수를 만들고 Book 타입에 없는 프로퍼티를 작성하게 되면 오류가 발생합니다. Book 타입의 서브 타입이 ProgrammingBook 타입이라 업캐스팅이 가능한데, Book 타입의 변수에 Book 타입에 없는 skill 타입을 작성하는 것이 무엇이 문제일까요? 초과 프로퍼티 검사의 기능이 바로 여기서 나타납니다. 이 기능은 타입에 정의된 프로퍼티 외의 다른 초과된 프로퍼티를 갖는 객체를 변수에 할당하지 못하도록 막습니다. 따라서 위 코드는 Book 타입에 정의되지 않은 skill 프로퍼티를 갖는 객체를 항당하려고 하였기 때문에 초과 프로퍼티 검사가 실패하여 오류가 발생하게 되는 것입니다.
let book3: Book = programmingBook;
function func(book: Book) {}
func({
name: "한 입 크기로 잘라먹는 리액트",
price: 33000,
skill: "react.js", // ❌
});
func(programmingBook);
하지만 다음과 같이 별도의 다른 변수에 보관한 다음 변수 값을 초기화 값으로 사용하면 초과 프로퍼티 검사는 발생하지 않습니다. 함수의 매개변수에 인수로 값을 전달하는 과정도 변수를 초기화하는 과정과 동일합니다. 따라서 초과 프로퍼티 검사가 발동하게 되는데 이때에도 역시 검사를 피하고 싶다면 변수에 미리 값을 담아둔 다음 변수값을 인수로 전달하면 됩니다.
💡 타입 계층도와 함께 기본 타입 살펴보기
• unknown 타입 (전체 집합)
unknown 타입은 타입 계층도의 최 상단에 위치합니다. 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 타입으로 업 캐스팅할 수 있는 것입니다.
let unknownVar: unknown;
let num: number = unknownVar; // ❌
let str: string = unknownVar; // ❌
let bool: boolean = unknownVar; // ❌
다운캐스트는 거의 허용되지 않는다고 하였기 때문에 unknown 타입의 값은 any를 제외한 어떤 타입의 변수에도 저장될 수 없습니다.
• never 타입 (공집합)
never 타입은 타입 계층도에서 가장 아래에 위치합니다. never 는 불가능을 의미하는 타입인데 집합의 개념을 도입하면 공집합을 의미합니다. 이는 아무것도 포함하지 않는 집합이라는 것입니다.
function neverFunc(): never {
while (true) {}
}
neverFunc 함수는 무한 루프를 돌기 때문에 정상적으로 종료되지 않으며 어떠한 값도 반환할 수 없습니다. 만약 이 함수가 어떤 값을 반환한다면 그것 또한 불가능하고 모순입니다.
function neverFunc(): never {
while (true) {}
}
// never는 모든 타입의 서브타입이기 때문에 업캐스팅 가능하다.
let num: number = neverFunc();
let str: string = neverFunc();
let bool: boolean = neverFunc();
// 숫자, 문자열, 불리언은 never로 다운캐스팅 불가능하다.
let never1: never = 10; // ❌
let never2: never = "string"; // ❌
et never3: never = true; // ❌
또한, 공집합은 모든 집합의 부분집합입니다. never 타입은 모든 타입의 서브 타입이기 때문에 never 타입은 모든 타입으로 업캐스팅할 수 있습니다. 하지만 그 어떠한 타입도 never 타입으로 다운 캐스팅하는 것은 불가능합니다.
• void 타입
void 타입은 아무것도 반환하지 않는 함수의 반환값 타입으로 주로 사용되는데 타입계층도에서도 void 타입을 보면 undefined 타입의 슈퍼타입임을 알 수 있습니다.
function voidFunc1(): void {
return undefined;
}
function voidFunc2(): void {
return;
}
function voidFunc3(): void {}
따라서 반환값이 void인 voidFunc1 함수에서 undefined를 반환하여도 오류가 발생하지 않습니다. undefined 타입은 void 타입의 서브 타입이므로 업캐스팅이 가능하기 때문입니다. void 타입의 서브타입은 undefined 타입과 never 타입으로 2가지 뿐입니다. 따라서 void 타입에는 undefined, never 외에 다른 타입의 값을 할당하는 것은 불가능합니다.
• 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 (업 캐스트)
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 타입 단언과 타입 좁히기 활용법, 서로소 유니온 타입의 이해 (0) | 2024.12.19 |
---|---|
[ TypeScript ] 대수 타입 & 타입 추론: 강력한 타입 시스템 이해하기 (0) | 2024.12.19 |
[ TypeScript ] any, unknown, void, never : 헷갈리는 타입 한눈에 보기 (0) | 2024.12.18 |
[ TypeScript ] 타입 설계: 타입 별칭, 인덱스 시그니처, 열거형 타입으로 코드 최적화하기 (0) | 2024.12.18 |
[ TypeScript ] 배열과 튜플, 그리고 객체 이해하기 (0) | 2024.12.18 |