프론트엔드/TypeScript

[TypeScript] Interface와 Type Alias의 차이점

인터페이스와 타입 앨리어스는 직접 타입을 정의할 수 있다는 점에서 유사합니다.

하지만 각각의 특징과 차이점이 존재하기 때문에 제대로 알고 사용하는 것이 좋습니다.

 

이 글을 통해 인터페이스와 타입 앨리어스, 그리고 둘의 차이점에 대해 알아보겠습니다.

 


인터페이스 (Interface)

객체는 넓게 묶으면 Object 타입이 되지만 사실 객체의 내부 구조는 제각각 다릅니다.

인터페이스는 객체가 가질 수 있는 다양한 구조들을 직접 타입으로 만들 수 있는 방법입니다.

이를 통해 객체 타입을 구체화할 수 있고, 인터페이스를 재사용할 수 있다는 장점이 있습니다.

 

 

기본 구조

interface 키워드를 통해 인터페이스를 정의할 수 있습니다.

인터페이스 내부는 단지 프로퍼티와 메서드의 타입에 대해서 정의만 합니다. (구현 X)

interface Student {
  studentId: number,
  studentName: string,
  age: number,
  graduated: boolean,
}

 

인터페이스는 세미콜론(;)이나 컴마(,)나 기호를 붙이지 않고 선언할 수 있습니다.

interface User {
  name: string,
  age: number
}

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

interface User {
  name: string
  age: number
}

 

 

선택적 프로퍼티 (Optional Properties)

물음표(?)를 통해 필수로 구현되지 않아도 되는 프로퍼티를 정의할 수 있습니다.

interface User {
  username: string;
  password: string;
  age?: number;
  address?: string;
}

const kim: User = {
  username: 'kim',
  password: '123456',
};

console.log(kim.phone); // Error : Property 'phone' does not exist on type 'User'

 

 

읽기 전용 프로퍼티

readonly 키워드를 통해 해당 값이 읽기만 가능하고 수정되지 않게 만들 수 있습니다.

interface Student {
  readonly studentId: number;
  studentName: string;
  age: number;
  subject?: string;
  graduated: boolean;
}

const func = (student: Student) => {
  student.studentId = 40; // Error : Cannot assign to 'studentId' because it is a read-only property
};

 

 

인덱서블 타입 (Indexable Type)

객체나 배열처럼 특정 구조를 가지는 수많은 값들이 올 수 있는 구조들이 존재합니다.

이렇게 인덱싱이 가능한 자료형 타입은 인덱스 시그니처를 통해 정의할 수 있습니다.

  • 인덱스 시그니처(Index Signature): 반복되는 <Key, Value> 형식을 [key: Type]: U로 표현하는 기법

인덱스 시그니처의 key 타입은 string number만 사용 가능합니다.

interface StringArray {
  [index: number]: string;
}

const arr: StringArray = ['a', 'b', 'c'];

 

 

변수 인터페이스

인터페이스를 선언받은 변수는 인터페이스를 준수하며 구현해야 합니다.

interface Todo {
  id: number;
  content: string;
  completed: boolean;
}

const newTodo: Todo = { id: 1, content: 'typescript', completed: false };

 

 

함수 인터페이스

함수도 객체이기 때문에 인터페이스를 통해 타입으로 정의할 수 있습니다.

함수 타입을 인터페이스로 정의하려면 호출 시그니처를 사용합니다.

  • 호출 시그니처(Call signature): 함수의 매개 변수와 반환 타입을 지정한 구조
interface Sum {
  (a: number, b: number): number; // 호출 시그니처 (Call signature)
}

const sum: Sum = (a, b) => {
  return a + b;
};

 

또한 인터페이스 내부에서의 함수 타입 표현은 2가지 방법으로 가능합니다.

interface Func {
  sum(a: number, b: number): number;
  sum: (a: number, b: number) => number;
}

 

 

클래스 인터페이스

인터페이스는 추상 클래스와 유사합니다.

프로퍼티와 메서드의 타입을 정의할 뿐, 실제로 인스턴스는 생성하지 않기 때문입니다.

 

인터페이스는 클래스 선언문의 implements 뒤에 선언합니다.

해당 클래스는 인터페이스의 프로퍼티와 추상 메서드를 반드시 구현해야 합니다.

interface ITodo {
  id: number;
  content: string;
  completed: boolean;
}

class Todo implements ITodo {
  constructor (
    public id: number,
    public content: string,
    public completed: boolean
  ) {}
}

const todo = new Todo(1, 'Typescript', false);

 

 

인터페이스 확장

extends 키워드를 사용하여 인터페이스를 확장할 수 있습니다.

이를 통해 인터페이스를 재사용할 수 있다는 장점이 존재합니다.

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

interface Student extends Person {
  grade: number;
}

const student: Student =  {
  name: 'Lee',
  age: 20,
  grade: 3
}

 

또한 컴마(,)를 통해 여러 개의 인터페이스를 확장할 수도 있습니다.

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

interface Developer {
  skills: string[];
}

interface WebDeveloper extends Person, Developer {}

const webDeveloper: WebDeveloper = {
  name: 'Lee',
  age: 20,
  skills: ['HTML', 'CSS', 'JavaScript']
}

 

 

선언 병합 (Declaration Merging)

같은 이름의 인터페이스를 정의된 인터페이스는 서로 병합됩니다.

이는 인터페이스의 확장에 있어 유리합니다.

그래서 외부에 공개할 API나 라이브러리는 인터페이스를 통해 선언 병합을 활용하는 것이 좋습니다.

interface Box {
  height: number;
  width: number;
}

interface Box {
  scale: number;
}

const box: Box = {
  height: 5, 
  width: 6, 
  scale: 10
};

 

 


타입 앨리어스 (Type Aliases)

타입 앨리어스(타입 별칭)는 타입을 정의한다는 점에서 인터페이스와 유사합니다.

 

 

기본 구조

type 키워드를 통해 정의할 수 있습니다.

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

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

 

 

활용 예시

주로 원시 값, 유니온 타입, 튜플 등의 다양한 타입의 조합을 구성할 때 활용됩니다.

type Str = 'Lee';

// 유니온 타입
type Union = string | null;

// 문자열 유니온 타입
type Name = 'Lee' | 'Kim';

// 숫자 리터럴 유니온 타입
type Num = 1 | 2 | 3 | 4 | 5;

// 객체 리터럴 유니온 타입
type Obj = {a: 1} | {b: 2};

// 함수 유니온 타입
type Func = (() => string) | (() => void);

// 인터페이스 유니온 타입
type Shape = Square | Rectangle | Circle;

// 튜플
type Tuple = [string, boolean];

 

 


차이점

선언 병합 유무

인터페이스는 같은 이름으로 여러 번 선언해도 선언 병합이 일어납니다.

반면 타입 앨리어스는 같은 이름으로 선언 시 식별자 중복 에러가 발생합니다.

interface Animal {
  weight: number;
}

interface Animal {
  height: number;
}
// Error
type Animal = {
  weight: number;
};

type Animal = {
  height: string;
};

 

 

확장 방법

인터페이스는 extends를 사용하여 확장합니다.

반면 타입 앨리어스는 & 기호를 사용하여 확장합니다.

interface Student extends People {
  school: string
}
type Student = People & {
  school: string
}

 

 

(이 글에 더 많은 차이점들이 정리되어 있어 참고가 되었습니다.)

 


결론

대부분의 상황에선 인터페이스를 권장합니다.

특히 외부에 공개할 API, 라이브러리는 선언 병합을 위해 인터페이스를 사용하는 것이 좋습니다.

반면 유니온, 튜플이 필요한 타입은 타입 앨리어스를 사용하는 것이 좋습니다.

 

결론적으로 프로젝트 또는 팀에서 이에 대한 규칙을 미리 정하는 것을 추천합니다.