너굴 개발 일지

[TypeScript] Type, Interface 중 뭘 써야할까? 본문

TypeScript

[TypeScript] Type, Interface 중 뭘 써야할까?

너굴냥 2023. 12. 13. 23:19

type vs interface

프로젝트할 때 보통 Interface로 데이터 타입을 정의했었는데 문득 Type, Interface의 차이와 언제 써야하는지 궁금해져 정리한 글입니다.

목차

본문

결론

 


Type Alias는 무엇일까?

타입의 새로운 이름을 만드는 역할로 실제로 새로운 타입을 만드는 것은 아니다. 인터페이스와 유사하지만 윈시 값, 유니언, 튜플 그리고 손으로 작성해야 하는 다른 타입의 이름을 지을 수 있다.

type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver
function getName(n: NameOrResolver): Name {
  if (typeof n === 'string') {
    return n
  } else {
    return n()
  }
}

type Tuple = [string, boolean];
const t: Tuple = ['', false];

Interface는 무엇일까?

상호 간에 정의한 약속 혹은 규칙을 의미한다. 타입스크립트에서 인터페이스는 보통 다음과 같은 범주에 대해 약속을 정의할 수 있다.

  • 객체의 스펙 (속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙 (파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스
interface PersonAge {
  age: number
}

function logAge(obj: PersonAge) {
  console.log(obj.age)
}
const person = { name: 'Capt', age: 28 }
logAge(person)

Type Alias, Interface의 공통점은 무엇일까?

타입 지정

Type Alias와 Interface 둘 다 타입에 대해 비슷한 방식으로 이름을 지어줄 수 있다.

interface Person {
  name: string
  age: number
}

const Kevin: Person = {
  name: 'Kevin',
  age: 20
}

type Person = {
  name: string
  age: number
}

const Kevin: Person = {
  name: 'Kevin',
  age: 20,
}

여러 타입에 대한 관계 정의

Interface에 대한 extends와 class에 대한 implements 키워드를 사용해 관계를 정의할 때 결합 타입(Union Type)이 아닌 객체 타입이나 객체 타입간의 교차 타입(Intersection Type), 즉 정적으로 모양을 알 수 있는 객체 타입에만 동작한다.

type TBase = {
  t: number;
};

interface IBase {
  i: number;
}

// extends 사용
interface I1 extends TBase {}

interface I2 extends IBase {}

// implements 사용
class C1 implements TBase {
  constructor(public t: number) {}
}

class C2 implements IBase {
  constructor(public i: number) {}
}

// 곱 타입에 대한 extends, implements 사용
type TIntersection = TBase & {
  t2: number;
};

interface I3 extends TIntersection {}

class C3 implements TIntersection {
  constructor(public t: number, public t2: number) {}
}

interface I4 extends I3 {}

class C4 implements I3 {
  constructor(public t: number, public t2: number) {}
}

// 오류: 합 타입에 대한 extends, implements 사용
type TUnion =
  | TBase
  | {
      u: number;
    };

interface I5 extends TUnion {}
// error(TS2312)
// An interface can only extend an object type
// or intersection of object types with statically known members.

class C5 implements TUnion {}
// error(TS2422)
// A class can only implement an object type
// or intersection of object types with statically known members.

 

Type Alias, Interface의 차이점은 무엇일까?

사용 데이터 형태

Interface는 객체 형태에 이름을 부여하는 것만 가능하지만, Type Alias은 객체 형태는 물론 모든(any)타입에 이름을 달아줄 수 있다.

또한 string과 number 둘 중 하나 아무거나 들어와도 괜찮을 경우 Union Type을 만들 수 있는데 interface를 이용해 타입을 정의하기 쉽지 않다. Intersection Type에서도 type과 &를 이용하는데 이렇게 계산이 필요한 타입은 Interface가 아닌 Type Alias를 이용한다.

interface Person {
    name: string;
    age: number;
}

interface UnionIf {
    string | number
}
// error(TS1131)
// Property or signature expected

type UnionType = string | number

type IntersectionType = string & number

선언 병합

Interface가 가지는 대부분의 기능은 Type Alias에서도 동일하게 사용 가능하다.

하지만 이 둘의 가장 핵심적인 차이는, 공식 문서에도 나와있듯이 Type Alias는 새 프로퍼티를 추가하도록 개방될 수 없는 반면, Interface 경우 항상 확장될 수 있다는 점이다.

동일한 이름으로 여러 번 선언해도 컴파일 시점에 합쳐지는 병합을 선언 병합(Declaration Merging)이라 하며 Type Alias에서는 불가하지만 Interface에서는 가능하다.

interface Person {
    name: string
}

interface Person {
    age: number
}

const kevin: Person = {
    name: 'kevin',
    age: 20
}

type Person {
    name: string
}

type Person {
    age: number
}
// error(TS2300)
// Duplicate identifier 'Person'

TypeScript 팀은 개방-폐쇄 원칙에 따라 확장에 열려있는 JavaScript 객체의 동작 방식과 비슷하게 연결하도록 Interface를 설계했다.

따라서 TypeScript 팀은 가능한 Type Alias보단 Interface를 사용하고, Union Type 혹은 Tuple Type을 사용해야 하는 경우 Type Alias를 사용하도록 권장하고 있다.

참고로 선언 병합은 라이브러리를 사용하는 상황에서 추가적으로 타입의 속성들을 선언할 때 유용하다.

// @emotion/react/types
export interface Theme {}

// emotion.d.ts
import '@emotion/react';

declare module '@emotion/react' {
  export interface Theme {
    colors: typeof Colors;
  }
}

emotion의 Type Alias를 보면 Interface를 통해 Theme이라는 타입을 제공해준다. emotion 라이브러리를 사용하는 해당 타입에 선언 병합을 활용해 본인들이 원하는 속성들을 선언해 사용할 수 있다.

 

왜 굳이 확장 가능한 방법과 확장이 가능하지 않은 방법으로 나눴을까?

두 가지 모두 비슷한 역할을 하니 헷갈리지 않게 한 가지 방법만 존재해도 되지 않나? 라는 생각을 했었다.
하지만 두 가지 방법을 모두 제공했다는 건 필요에 의해 만들어졌다는 생각도 든다.

만약 모든 타입의 정의를 Interface로만 했다면 확장에 유연할 수 있지만 타입의 속성들이 추후에 추가될 수도 있다는 생각을 항상 하면서 개발을 해야 할 수도 있다. 따라서 확장에 덜 유연한 Type Alias을 통해 정적인 타입과 확장 가능성이 있는 타입을 구분할 수 있지 않나 싶다. (마치 let보다는 const를 이용해 상수로 선언해주는 게 좋은 이유가 비슷한 맥락이 아닐까 싶다.)

 

Type Alias와 Interface 중 뭘 사용해야할까?

팀 레벨 혹은 프로젝트 레벨에서 지정한 컨벤션에 따라 일관성 있게 사용해야겠지만 일반적인 상황이라면 아래와 같이 정의할 수 있을 것 같다.

  • Union, Tuple Type이 필요한 경우 혹은 어떤 값에 대한 정의같이 정적으로 결정되어 있는 것은 Type Alias 사용하기
  • 외부에 공개할 API이거나(선언 병합을 위해) 확장될 수 있는 basis를 정의하는 경우 Interface를 사용하기

참고 자료

https://www.typescriptlang.org/docs/handbook/advanced-types.html#interfaces-vs-type-aliases

 

Documentation - Advanced Types

Advanced concepts around types in TypeScript

www.typescriptlang.org

https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html#%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4

 

Documentation - Everyday Types

언어의 원시 타입들.

www.typescriptlang.org

https://www.typescriptlang.org/docs/handbook/2/objects.html#interfaces-vs-intersections

 

Documentation - Object Types

How TypeScript describes the shapes of JavaScript objects.

www.typescriptlang.org

https://timmousk.com/blog/typescript-intersection-type/

 

What Is An Intersection Type In Typescript?

An in-depth article on what is an intersection type in TypeScript. What is the difference between an intersection type vs extends?

timmousk.com