[TS] 타입 에러가 발생한다면? 타입 호환 확인하기

chaevivi·2023년 11월 29일
0
post-thumbnail

타입 호환


1. 타입 호환이란?

타입 호환이란 서로 다른 2개의 타입이 있을 때 한 타입이 다른 타입에 포함되는지를 의미합니다.

타입 호환은 구조적 서브타이핑(subtyping)에 기반합니다. 구조적 타이핑은 멤버만을 기준으로 타입들을 연관시키는 방법입니다.

예를 들어,

interface Pet {
  name: string;
}

class Dog {
  name: string;
}

let pet: Pet;
pet = new Dog();    // 가능
  • 똑같이 string 타입인 name 속성을 갖는 Pet 인터페이스와 Dog 클래스를 선언하였습니다.
  • Dog 클래스는 명시적으로 Pet 인터페이스를 상속받지 않았기 때문에 에러가 발생할 것이라고 생각할 수 있지만 결과적으로 에러가 발생하지 않습니다.
  • 다시 말해, 타입스크립트의 타입 호환은 타입 구조로 호환 여부를 판별하는 구조적 타이핑을 기반으로 하고 있습니다.


2. 함수의 타입 호환


2.1. 함수의 매개변수의 타입 호환

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x;
x = y;    // Error
  • xnumber 타입의 매개변수를 가지고 있고, ynumberstring 타입을 가진 매개변수를 가지고 있습니다.
  • y = x
    • x의 매개변수는 y의 매개변수와 호환 가능한 타입을 가지고 있기 때문에 할당이 가능합니다.
    • ⚠️ 매개변수의 이름은 고려하지 않고 타입만 검사합니다.
  • x = y
    • x 매개변수는 ystring 타입을 가지고 있지 않기 때문에 할당이 불가능해 에러가 발생합니다.

그렇다면 xy의 매개변수 타입이 정확하게 일치하지 않는데 y = x 할당이 가능한 이유는 무엇일까요?
▶️ 바로 자바스크립트에서는 함수의 추가 매개변수를 무시하기 때문입니다.


let items = [1, 2, 3];

items.forEach((item, index, array) => console.log(item));
items.forEach(item => console.log(item));     // 가능

예를 들어, forEach 메서드는 콜백 함수에서 사용할 수 있는 배열 요소, 요소의 인덱스, 그리고 해당 배열을 매개변수로 제공합니다. 하지만 forEach에서는 사용하지 않는 매개변수는 무시하기 때문에 하나의 매개변수만 사용하여도 에러가 발생하지 않습니다.


2.2. 함수의 반환 타입 호환

함수의 반환 타입 호환은 매개변수의 타입 호환과 다릅니다.

let x = () => ({ name: "Dove" });
let y = () => ({ name: "Dove", location: "Suwon" });

x = y;
y = x;    // Error
  • x = y
    • y에는 xname 프로퍼티를 가지고 있기 때문에 할당이 가능합니다.
  • y = x
    • x에는 yname 프로퍼티는 가지고 있지만 location 프로퍼티가 없기 때문에 할당이 불가능합니다.


3. 객체의 타입 호환

type Person = {
  name: string;
};

interface Developer {
  name: string;
  skill: string;
}

let dove: Person = {
  name: 'dove'
};

let nanami: Developer = {
  name: 'nanami',
  skill: 'ts'
};

dove = nanami;
nanami = dove;    // Error
  • Personname 속성을 가지고 있고, Developernameskill 속성을 가지고 있습니다.
  • dove 변수에 Person 타입을 선언하고 nanami 변수에는 Devloper 타입을 선언하고 값을 할당하였습니다.
  • dove = nanami
    • dove 변수의 Person 타입은 nanami 변수의 Devleoper 타입의 name 속성을 가지고 있기 때문에 타입 호환이 가능합니다.
  • nanami = dove
    • nanami 변수의 Developer 타입은 nameskill 속성이 모두 선언되어야 하는데 dove 변수의 Person 타입은 name 속성만 있기 때문에 타입 호환이 불가능합니다.

nanami = dove의 에러를 해결하고 싶다면 어떻게 해야 할까요?

type Person = {
  name: string;
};

interface Developer {
  name: string;
  skill?: string;
}

let dove: Person = {
  name: 'dove'
};

let nanami: Developer = {
  name: 'nanami',
  skill: 'ts'
};

dove = nanami;
nanami = dove;   
  • Developer 타입의 skill 속성을 옵셔널로 변경하면 타입 에러를 해결할 수 있습니다.

4. 이넘의 타입 호환


4.1. 숫자형 이넘과 number의 타입 호환

enum Language {
  C,          // 0
  Java,       // 1
  TypeScript  // 2
}

let a: number = 10;
a = Language.C;
Language.C = a;  // Error
  • a = Language.C
    • number 타입의 변수 a에 10을 할당하고 Language 이넘의 속성 C를 할당하면 에러가 발생하지 않습니다.
    • 숫자형 이넘은 숫자 타입과 호환되기 때문입니다.
  • Language.C = a
    • Language 이넘의 속성 Ca를 할당하면 에러가 발생합니다.
    • 이넘은 값을 읽는 것만 가능하고 쓰는 것은 불가능하기 때문입니다.

4.2. 이넘 간의 타입 호환

enum Language {
  C, 
  Java,
  TypeScript
}

enum programming {
  C,
  Java,
  TypeScript
}

let langC: Language.C;
langC = Programming.C;    // Error
  • langC 변수에 Language 이넘의 속성 C를 타입으로 선언하고 해당 변수에 Programming 이넘의 속성 C를 할당하면 에러가 발생합니다.
  • 이넘 타입은 같은 속성과 값을 가졌더라도 이넘 타입은 서로 호환되지 않습니다.


5. 제네릭의 타입 호환

제네릭의 타입 호환은 구조적 타입에 기반하고 있기 때문에 제네릭으로 받은 타입이 타입 구조 내에서 사용되었는지의 여부를 확인합니다.


interface Empty<T> { }

let x: Empty<number>;
let y: Empty<string>;

x = y;
y = x;
  • xy 변수는 Empty 인터페이스의 제네릭 타입을 사용합니다.
  • Empty 인터페이스는 제네릭으로 타입을 넘겨받아도 타입 구조에 영향을 끼치지 않으므로 서로 타입이 호환됩니다.

interface NotEmpty<T> {
  data: T;
}

let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // Error
  • NoEmpty 인터페이스는 제네릭으로 타입을 넘겨 받고 해당 타입을 data 속성에서 사용합니다.
  • NoEmpty 인터페이스는 제네릭으로 타입을 넘겨 받으면 data 속성에서 사용하기 때문에 (타입 구조 안에서 사용했기 때문에) 에러가 발생합니다.



참고
📖 쉽게 시작하는 타입스크립트
🔗 https://www.typescriptlang.org/ko/docs/handbook/type-compatibility.html

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글