TypeScript type vs interface — 뭐가 다르고 언제 쓸까

TypeScript 인터페이스와 타입 별칭(type alias)의 차이를 물어보면 대부분 “interface는 extends, type은 &”라고 답한다. 틀린 말은 아니다. 그런데 이 답만으로는 실무에서 뭘 선택해야 할지 판단이 안 된다.

결론부터 말하면 기본 객체 타입 정의는 둘 다 완전히 같다. 차이가 생기는 건 세 가지 상황이다 — Declaration merging, Discriminated Union, 유니온 타입. 이 세 가지를 모르면 둘을 무작위로 섞다가 의도하지 않은 타입 합산이나 표현 불가 패턴을 만나게 된다.

TypeScript interface extends 확장과 type 유니온 패턴 코드 비교

interface와 type의 공통점

기본 객체 타입 정의는 둘이 완전히 동일하다.

// 동일한 결과를 만든다
interface User {
  id: number;
  name: string;
  email: string;
}

type User = {
  id: number;
  name: string;
  email: string;
}

클래스 implements도 둘 다 된다.

class AdminUser implements User {
  id = 1;
  name = '관리자';
  email = '[email protected]';
}

함수 타입도 둘 다 표현할 수 있다.

interface Formatter {
  (input: string): string;
}

type Formatter = (input: string) => string;

객체 형태를 다루는 대부분의 상황에서 interfacetype은 바꿔 써도 된다. 차이는 아래 두 섹션에서 나온다.


interface만 할 수 있는 것

Declaration merging (선언 병합)

interface는 같은 이름으로 여러 번 선언하면 자동으로 합쳐진다.

interface Config {
  timeout: number;
}

interface Config {
  retries: number;
}

// 결과: { timeout: number; retries: number; }
const config: Config = { timeout: 3000, retries: 3 };

라이브러리 타입을 확장할 때 이 기능을 쓴다. 전역 타입에 손대지 않고 내 프로젝트에서만 속성을 추가할 수 있다.

// 전역 Window에 커스텀 속성 추가
declare global {
  interface Window {
    analytics: Analytics;
  }
}

반대로 type은 중복 선언 시 컴파일 에러가 난다.

type Config = { timeout: number };
type Config = { retries: number }; // Error: Duplicate identifier 'Config'

Declaration merging의 함정이 있다. 실수로 같은 이름을 두 번 선언해도 에러가 없다는 점이다. 팀이 커지거나 라이브러리를 합칠 때 조용히 타입이 붙어버리는 버그의 원인이 된다. type은 중복 선언 즉시 에러가 나서 이 실수를 잡아준다.

interface extends — 확장과 충돌 즉시 감지

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

type&(Intersection)으로 비슷하게 표현하지만 에러 처리 방식이 다르다. interface extends는 호환되지 않는 프로퍼티가 있으면 즉시 컴파일 에러를 낸다. type &는 충돌 시 해당 프로퍼티가 never 타입이 돼서 런타임에서야 이상함을 발견하게 된다.

interface A { x: string }
interface B { x: number }
interface C extends A, B {} // Error: 즉시 충돌 감지

type C = A & B; // 에러 없음 — C.x가 never가 된다

type만 할 수 있는 것

유니온 타입

type은 유니온(|)을 직접 표현할 수 있다. interface로는 불가능하다.

type Status = 'loading' | 'success' | 'error';
type ID = string | number;
type Nullable<T> = T | null;

Discriminated Union (판별 유니온)

type의 킬러 피처다. API 응답 처리나 상태 관리에서 특히 유용하다.

type ApiResponse =
  | { status: 'success'; data: User[] }
  | { status: 'error'; message: string }
  | { status: 'loading' };

function handleResponse(res: ApiResponse) {
  if (res.status === 'success') {
    console.log(res.data); // data 타입 자동 추론
  } else if (res.status === 'error') {
    console.log(res.message); // message 타입 자동 추론
  }
}

status 필드를 기준으로 각 분기의 타입을 컴파일러가 자동으로 좁혀준다. interface로는 이 패턴을 표현할 수 없다.

튜플, 원시 타입 별칭, 유틸리티 타입

type Point = [number, number];       // 튜플
type Pair<T> = [T, T];
type StringOrNumber = string | number;

// Mapped type — TypeScript 내장 유틸리티가 이걸로 만들어져 있다
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// Conditional type
type NonNullable<T> = T extends null | undefined ? never : T;

Partial, Required, Pick, Omit 같은 TypeScript 내장 유틸리티 타입이 모두 type으로 구현된 이유가 여기에 있다.


TypeScript type vs interface 핵심 차이 비교

기능interfacetype
기본 객체 타입 정의가능가능
extends 확장가능 (충돌 즉시 감지)&로 가능 (충돌 시 never)
Declaration merging가능불가
유니온 타입불가가능
Discriminated Union불가가능
튜플불가가능
Mapped type불가가능
Conditional type불가가능
class implements가능가능
컴파일 성능약간 빠름약간 느림

컴파일 성능 차이는 대형 코드베이스(수백 개 파일 이상)에서 체감 가능한 수준이다. TypeScript 컴파일러는 interface 이름을 캐싱해서 재사용한다. type 별칭은 매번 인라인 확장을 거친다. 일반 프로젝트에서는 무시해도 된다.


TypeScript type vs interface 실무 선택 기준

TypeScript 공식 핸드북의 권장은 명확하다. 특별한 이유가 없으면 interface를 써라. type이 필요한 상황이 되면 그때 type으로 바꿔라.

실무에서 이 기준을 구체화하면:

interface를 쓰는 상황:

  • 컴포넌트 props, 함수 인자, API 응답 모델처럼 “객체 형태”를 정의할 때
  • 라이브러리를 만들거나 타입을 외부에 공개할 때 (사용자가 Declaration merging으로 확장할 수 있게)
  • 클래스가 구현해야 할 계약을 정의할 때

type을 쓰는 상황:

  • 유니온 타입이 필요할 때 ('loading' | 'success' | 'error')
  • Discriminated Union 패턴으로 API 응답이나 상태를 모델링할 때
  • 튜플, 함수 타입 별칭을 간결하게 표현할 때
  • Mapped type, Conditional type 등 유틸리티 타입을 직접 만들 때

배열 메서드에 TypeScript 타입을 붙이는 경우에도 판단 기준이 같다. map의 반환 타입은 type으로 정의하고, props나 모델은 interface로 가져간다. map과 forEach의 TypeScript 타입 차이에서 이 맥락을 코드로 볼 수 있다.


자주 묻는 질문

팀 컨벤션으로 하나만 써야 하나요?

일관성 자체보다 팀이 판단 기준을 공유하는 게 더 중요하다. ESLint @typescript-eslint/consistent-type-definitions 규칙으로 기본값을 강제할 수 있다. interface로 설정하면, interface로 표현 가능한 곳에서 type을 쓸 때 경고가 뜬다.

React에서 props 타입은 뭘 써야 하나요?

interface를 더 많이 쓴다. 에러 메시지가 더 명확하다는 이유에서다. interface ButtonProps로 선언하면 에러 메시지에 ButtonProps가 그대로 보인다. type은 경우에 따라 인라인 타입이 펼쳐져서 에러가 길어진다.

// 둘 다 동작하지만 에러 메시지 가독성이 다르다
interface ButtonProps {
  label: string;
  onClick: () => void;
}

Declaration merging이 실수로 일어나면 어떻게 되나요?

에러가 없다는 게 문제다. 라이브러리와 프로젝트 코드에서 같은 이름의 interface를 선언하면 조용히 합쳐진다. 갑자기 없어야 할 프로퍼티가 타입에 생기거나 예상 밖의 자동완성이 뜨는 이유가 이것일 수 있다. type은 중복 선언 즉시 컴파일 에러가 나서 바로 알 수 있다.


한줄 정리: TypeScript type vs interface 선택 기준은 단순하다. 객체 형태 정의는 interface, 유니온·Discriminated Union·Mapped type이 필요하면 type. 특별한 이유 없으면 interface가 기본이다.

댓글 남기기