goongoguma's blog

Types vs. interfaces in TypeScript

자바스크립트에서 정적 타입 체크를 한다는 정말 좋은 아이디어였습니다. 그리고 타입스크립트가 적용이 되는 상황은 시간이 지날수록 증가하고 있습니다.

프로젝트에서 타입스크립트를 사용하기 시작했고, 첫번째 타입(type)을 만들었습니다. 그리고 첫번째 인터페이스(interface)를 만들어보고 잘 작동하게 만들었습니다. 그래서 타입스크립트는 개발에 도움을 주고 소중한 시간을 아낄 수 있게 해준다고 생각했습니다. 하지만 타입스크립트의 타입과 인터페이스를 사용하기 시작했을때, 어떤 실수를 했을 수 있으며 모범 사례(best practice)를 지키지 않았을 경우도 있을겁니다.

위의 케이스는 많은 개발자들에게서 나타납니다. 많은 개발자들은 타입스크립트에서 제공하는 타입 별칭(type aliases)과 인터페이스의 진짜 차이점에 대해 잘 모르고 있습니다.

타입스크립트를 처음 시작할때는 그렇게 어렵지는 않지만 가끔 때에 맞는 사용 사례에 관해 생각해봐야 합니다. 이 글에서는 타입 혹은 인터페이스의 사용이 어떤 상황에서 적절한지 알아보도록 하겠습니다.

Types and type aliases

타입스크립트의 타입과 인터페이스의 차이점이 무엇인지 알아보기전에, 먼저 알아두어야 할 것이 있습니다.

타입스크립트에서는 string, boolean 그리고 number와 같이 많은 타입이 존재합니다. 이러한 타입들은 타입스크립트의 기본 타입(basic type)입니다. 여기서 기본 타입들의 리스트를 확인해 보실 수 있습니다. 또한, 타입스크립트에서 고급 타입(advanced type)이 존재합니다. 그리고 이러한 고급 타입타입 별칭(type aliases)이라고 불립니다. 타입 별칭을 사용하면, 타입의 새로운 이름을 만들어낼 수 있지만, 새로운 타입을 정의하지는 못합니다.

새로운 타입 별칭을 만들때 type 키워드를 사용합니다. 이 때문에 어떤 사람들은 혼동하고 새로운 타입을 생성했다고 생각 할 수 있습니다. 단지 타입에 새로운 이름을 붙여주는것 뿐인데 말이죠. 그래서 타입과 인터페이스의 차이점에 대해 다른 사람들이 이야기 하는것을 들을때마다, 이 사람들은 타입 별칭 vs 인터페이스에 관해 이야기 나누는구나 추측할 수 있을겁니다.

이 글에서 코드예시를 보여주기 위해 TypeScript Playground를 사용할겁니다. TypeScript Playground는 최신 버전(혹은 원하는 어떤 버전)의 타입스크립트를 테스트할 수 있게 해줍니다. 그리고 playground를 사용함으로써 새로운 타입스크립트 프로젝트를 생성해서 테스트하는것보다 시간을 아낄 수 있게 해주죠.

Types vs. interfaces

타입스크립트에서 인터페이스와 타입의 차이점은 예전에는 더욱 명확했지만 최신 버전에 들어오면서 비슷해졌습니다.

인터페이스는 기본적으로 데이터의 모양을 묘사하는 방법이라고 보시면 됩니다. 예를들면 객체가 있죠.

타입은 데이터 타입의 정의입니다. 예를들면 유니언(union), 기본형(primitive), 교차(intersection), 튜플(tuple) 혹은 그 외의 타입들이 있습니다.

Declaration merging

타입에서는 불가능하지만 인터페이스에서 할 수 있는 가능한 한가지는 바로 선언 병합(declaration merging)입니다. 선언 병합은 타입스크립트 컴파일러가 두개 혹은 그 이상의 같은 이름의 인터페이스들을 하나로 병합하면서 발생합니다.

예를들면 Song이라는 다른 프로퍼티들을 가진 두개의 인터페이스가 있다고 가정해봅시다:

interface Song {
  artistName: string;
}
interface Song {
  songName: string;
}

const song: Song = {
  artistName: "Freddie",
  songName: "The Chain",
};

타입스크립트는 자동적으로 두개의 인터페이스 선언을 하나로 병합할것이고 Song은 두개의 프로퍼티를 가지게 되었습니다.

타입을 사용해서 선언 병합을 할 수 없습니다. 만약 같은 이름을 가졌지만 다른 프로퍼티들을 가진 두개의 타입을 생성하려고 한다면 타입스크립트는 아래와 같은 에러를 발생합니다.

Duplicate identifier Song.

Extends and implements

타입스크립트에서는 인터페이스를 쉽게 확장하고 구현할 수 있습니다. 하지만 타입은 이와같이 사용이 불가능합니다.

타입스크립트에서의 인터페이스는 클래스를 확장할 수 있습니다. 이 기능은 객체지향 프로그래밍 방식을 도와주는 매우 훌륭한 컨셉입니다. 또한 인터페이스를 구현하는 클래스를 만들수도 있습니다.

예를 들어, Car라고 하는 클래스와 NewCar라고 불리는 인터페이스가 있다고 생각해봅시다. 인터페이스를 사용해서 해당 클래스를 쉽게 확장할 수 있습니다:

class Car {
  printCar = () => {
    console.log("this is my car");
  };
}

interface NewCar extends Car {
  name: string;
}

class NewestCar implements NewCar {
  name: "Car";
  constructor(engine: string) {
    this.name = name;
  }
  printCar = () => {
    console.log("this is my car");
  };
}

Intersection

교차 타입은 여러개의 타입을 하나의 타입으로 만들어줍니다. 교차 타입을 만들어주기위해, & 키워드를 사용합니다:

type Name = {
  name: string
};

type Age = {
  age: number
};

type Person = Name & Age;

교차 타입의 좋은점은 두개의 인터페이스를 결합하여 새로운 교차타입을 생성할 수 있다는 점입니다. 하지만 반대로는 불가능합니다. 두개의 타입을 결합하여 인터페이스를 생성할 수 없습니다. 왜냐하면 작동을 하지 않거든요.

interface Name {
  name: string
};

interface Age {
  age: number
};

type Person = Name & Age;

Unions

유니언 타입은 하나 혹은 조금 더 많은 값을 가지고 있는 타입을 사용해 새로운 타입을 만들 수 있게 해줍니다.

type Man = {
  name: string
};

type Woman = {
  name: string
};

type Person = Man | Woman;

교차 타입과 비슷하게, 두개의 인터페이스를 결합하여 새로운 유니언 타입을 생성할 수 있습니다. 하지만 역시 반대로 두개의 타입을 결합하여 인터페이스를 생성할 수는 없습니다.

interface Man {
  name: "string";
}

interface Woman {
  name: "string";
}

type Person = Man | Woman;

Tuples

튜플(Tuple)은 타입스크립트에서 매우 도움이 되는 컨셉입니다. 해당 타입은 서로 다른 데이터 유형의 두 가지 값 타입을 포함하고 있는 새로운 데이터 타입을 가져옵니다.

type Reponse = [string, number];

하지만 타입스크립트에서는 오직 타입만을 사용하여 튜플 타입을 선언할 수 있습니다만 인터페이스를 사용해서 튜플을 선언하는 방법도 있습니다. 인터페이스의 안에서만 튜플 사용이 가능합니다 아래와 같이요:

interface Response {
  value: [string, number];
}

계속해서 보셨다시피 타입과 인터페이스를 사용해서 같은 결과를 만들어낼 수 있습니다. 그래서, 여기 많은 개발자들이 가질법한 질문이 있습니다. 인터페이스 대신 타입을 사용하는게 좋을까요? 그렇다면 언제 타입을 사용해야 할까요?

둘 중 하나의 사용을 포기하지 않도록 두개의 개념에 대해 최상의 사용 사례를 알아봅시다.

What should I use?

이 질문은 굉장히 까다롭습니다. 답을 알려드리자면, 아마 예상하셨듯이, 여러분이 무엇을 만들고 어떤 일을 하는지에 따라 달렸습니다.

인터페이스는 새로운 객체나 객체안의 메서드를 정의할 때 사용하는것이 더 좋습니다. 예를 들어, 리액트 어플리케이션의 경우, 어떤 컴포넌트가 받아야 할 props의 정의가 필요할 때, 타입에 비해 인터페이스의 사용이 더 이상적이라고 할 수 있습니다.

interface TodoProps {
  name: string;
  isCompleted: boolean
};

const Todo: React.FC<TodoProps> = ({ name, isCompleted }) => {
  ...
};

타입은 함수를 생성할때 사용하는것이 더 좋습니다. 예를들어 호출된 객체를 반환하는 함수가 있다고 가정했을때, 별칭 타입의 사용이 더 권장됩니다.

type Person = {
  name: string,
  age: number,
};

type ReturnPerson = (person: Person) => Person;

const returnPerson: ReturnPerson = (person) => {
  return person;
};

타입 별칭을 사용할지 아니면 인터페이스를 사용할지 결정하기 위해서, 작업중인 코드나 특정코드등, 상황을 조심스럽게 생각하고 분석해봐야 합니다.

인터페이스는 객체, 메소드의 객체와 사용하는것이 좋습니다. 그리고 타입은 함수나 복잡한 타입등에 사용하는것이 좋습니다.

하나만을 사용하는것은 좋지 않습니다. 이렇게 하는 대신에 코드를 천천히 리팩토링 하고, 특정 상황에 어떤것을 사용하면 좋을지 생각해보세요.

두개를 모두 사용해도 괜찮다는것을 기억하세요. 이 글의 주제는 타입과 인터페이스의 차이점과 어떤 상황에서 사용해야 하는지 명확하게 하는것입니다.

Conclusion

이 글에서, 타입스크립트의 인터페이스와 타입의 차이점을 살펴보았습니다. 타입 별칭은 타입스크립트에서의 고급 타입이라고 배웠으며, 타입과 인터페이스 사용에서의 알맞는 사용사례를 배웠습니다. 그리고 어떻게 실제 프로젝트에서 적용할지도요.

원문: Types vs. interfaces in TypeScript by Leonardo Maldonado