Frontend/TypeScript

[TypeScript] 2-1. 타입스크립트 이해하기 - 타입 관계

ayeongjin 2025. 1. 23. 11:49

타입은 집합이다

  • 타입스크립트의 모든 타입들은 집합으로써 서로 포함하고 포함되는 관계를 갖는다.
  • 그리고 이런 관계에서 Number 타입처럼 다른 타입을 포함하는 타입을 **슈퍼 타입(부모 타입)**이라고 부릅니다. 반대는 **서브 타입(자식 타입)**이라고 한다.

1. 타입 호환성

A와 B 두개의 타입이 존재할 때 A 타입의 값을 B 타입으로 취급해도 괜찮은지 판단하는 것을 의미한다. 만약 A 타입의 값이 B 타입의 값으로 취급 되어도 괜찮다면 호환된다고 하고 안된다면 호환되지 않는다고 함.

  • 따라서 타입스크립트에서는 이렇게 슈퍼타입의 값을 서브타입의 값으로 취급하는것을 허용하지 않는다.
  • ex)
// 가능

let num1: number = 10;
let num2: 10 = 10;

num1 = num2;
// 불가능

let num1: number = 10;
let num2: 10 = 10;

num2 = num1;  // 오류
  • 업캐스팅 : 서브 타입의 값을 슈퍼 타입의 값으로 취급하는 것
  • 다운캐스팅 : 슈퍼 타입의 값을 서브타입의 값으로 취급 ( 대부분 불가능 )

2. unknown 타입 (전체 집합)

unknown 타입은 타입 계층도의 최 상단에 위치

  • unknown 타입 변수에는 모든 타입의 값을 할당할 수 있다. (모든 타입은 unknown 타입으로 업 캐스트 가능)
  • unknown 타입의 값은 any를 제외한 어떤 타입의 변수에도 할당할 수 없다.

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
let unknownValue: unknown;

let a: number = unknownValue;
// 오류 : unknown 타입은 number 타입에 할당할 수 없습니다.

3. never 타입 (공집합 타입)

  • never 타입은 타입 계층도에서 가장 아래에 위치합니다.
  • never 타입에 해당하는 값은 말 그대로 아무것도 없다.

ex.

  • errorFunc 함수는 에러를 발생시킴 → 이 함수는 정상적으로 종료되지 않는다.
  • 그러므로 어떤 값도 반환할 수 없다
function errorFunc(): never {
  throw new Error();
}
  • 쉽게 업캐스팅 가능
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
  • 다운캐스팅 불가능
let a: never = 1;                 // number -> never ❌
let b: never = "hello";           // string -> never ❌
let c: never = true;              // boolean -> never ❌
let d: never = null;              // null -> never ❌
let e: never = undefined;         // undefined -> never ❌
let f: never = [];                // Array -> never ❌
let g: never = {};                // Object -> never ❌

4. void 타입

아무것도 반환하지 않는 함수의 반환값 타입으로 주로 사용

ex.

function noReturnFunc(): void {
  console.log("hi");
}
  • void 타입은 undefined 타입과 never타입의 슈퍼타입이므로 반환값을 void로 선언한 함수에서 undefined을 반환 해도 오류가 발생하지 않음
function noReturnFuncA(): void {
  return undefined;
}

function noReturnFuncB(): void {
  return;
}

function noReturnFuncC(): void {}
  • void 타입의 서브타입은 undefined 타입과 never 타입 밖에 없다.
  • 따라서 void 타입에는 undefined, never 이외에 다른 타입의 값을 할당할 수 없다.
let voidVar: void;

voidVar = undefined; // undefined -> void (ok)

let neverVar: never;
voidVar = neverVar; // never -> void (ok)

5. any 타입

모든 타입으로 다운캐스트 할 수 있으며 또 모든 타입은 any 타입으로 업 캐스트 할 수 있다.

  • 모든 타입의 슈퍼타입이 될 수도 있고 모든 타입의 서브 타입이 될 수도 있음

  • 각각 number, string, boolean 타입을 갖는 변수 num, str, bool에 any 타입의 값을 할당
  • 이는 any 타입이 각각 number, string, boolean 타입으로 다운 캐스트 된다고 이해 가능함
  • any 타입 변수 anyValue에 num, str, bool 변수에 담긴 값을 할당합니다.
  • 이는 number, string, boolean 타입이 모두 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 (업 캐스트)

객체 타입의 호환성

1. 객체 타입의 호환성

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; // ✅ OK
dog = animal; // ❌ NO
  • Animal 타입의 변수 animal에 Dog 타입의 변수 dog를 할당하는 것은 가능
  • 그러나 반대로 dog 변수에 animal 변수의 값을 할당하는 것은 불가능
  • Animal 타입이 Dog 타입의 슈퍼타입이기 때문
💡
Animal 타입이 Dog 타입의 슈퍼타입인 이유
- 타입스크립트는 프로퍼티를 기준으로 타입을 정의하는 구조적 타입 시스템을 따른다.
- Animal 타입은 name과 color 프로퍼티를 갖는 모든 객체들을 포함하는 집합이고, Dog 타입은 name과 color 거기에다 추가로 breed 프로퍼티를 갖는 모든 객체를 포함하는 집합이다.
- 그러므로 어떤 객체가 Dog 타입에 포함된다면 무조건 Animal 타입에도 포함된다.
- 그러나 반대로 Animal 타입에 포함되는 모든 객체가 Dog 타입에 포함되는것은 아니다.

ex.

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

type ProgrammingBook = {
  name: string;
  price: number;
  skill: string;
};

let book: Book;
let programmingBook: ProgrammingBook = {
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
};

book = programmingBook; // ✅ OK
programmingBook = book; // ❌ NO

2. 초과 프로퍼티 검사

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

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

type ProgrammingBook = {
  name: string;
  price: number;
  skill: string;
};

let book2: Book = { // 오류 발생
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
};
  • 위 코드는 Book 타입에 정의되지 않은 skill 프로퍼티를 갖는 객체를 할당하려고 했으므로 초과 프로퍼티 검사가 실패해 오류가 발생
  • 이런 초과 프로퍼티 검사는 단순히 변수를 초기화 할 때 객체 리터럴을 사용하지만 않으면 발생하지 않는다.
  • 따라서 다음과 같이 값을 별도의 다른 변수에 보관한 다음 변수 값을 초기화 값으로 사용하면 발생하지 않음

ex.

let book3: Book = programmingBook; // 앞서 만들어둔 변수
  • 초과 프로퍼티 검사는 함수의 매개변수에도 동일하게 발생
function func(book: Book) {}

func({ // 오류 발생
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
});
  • 함수의 매개변수에 인수로 값을 전달하는 과정도 변수를 초기화 하는 과정과 동일함
  • 따라서 초과 프로퍼티 검사가 발동
  • 이때에도 역시 검사를 피하고 싶다면 다음과 같이 변수에 미리 값을 담아둔 다음 변수값을 인수로 전달하면 된다

ex.

func(programmingBook);

대수 타입

대수 타입이란 여러개의 타입을 합성해서 만드는 타입 (합집합은 Union 타입, 교집합은 Intersection 타입)

1. 합집합(Union) 타입

바 | 를 이용

// 합집합 타입 - Union 타입
let a: string | number;

a = 1;
a = "hello";
  • 변수 a에는 number 타입과 string 타입에 해당하는 값이라면 뭐든 저장할 수 있다.

1) Union 타입으로 배열 타입 정의하기

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

2) 객체타입의 Union타입

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

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

type Union1 = Dog | Person;

let union1: Union1 = { // ✅
  name: "",
  color: "",
};

let union2: Union1 = { // ✅
  name: "",
  language: "",
};

let union3: Union1 = { // ✅
  name: "",
  color: "",
  language: "",
};

let union4: Union1 = { // ❌
  name: "",
};

2. 교집합(Intersection) 타입

&을 이용

let variable: number & string;
// never 타입으로 추론된다
  • 서로 교집합을 공유하지 않는 서로소 집합이므로 변수 variable의 타입은 결국 never 타입으로 추론
  • 대다수의 기본 타입들 간에는 서로 공유하는 교집합이 없기 때문에 이런 인터섹션 타입은 보통 객체 타입들에 자주 사용됩니다.

1) 객체 타입의 Intersection 타입

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

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

type Intersection = Dog & Person;

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