Ch 06. 타입 별칭 vs 인터페이스
type과 interface — 둘 다 객체 타입을 정의하지만 동작 방식에 차이가 있습니다. 실무 선택 기준을 정리합니다.
type 별칭 기본
// 파일: src/type-alias.ts// 원시 타입에 이름 붙이기type UserId = number;type Email = string;// 객체 타입type User = { id: UserId; name: string; email: Email;};// 유니온 타입type Status = "active" | "inactive" | "banned";// 인터섹션 타입type AdminUser = User & { permissions: string[] };// 함수 타입type Handler<T, R> = (input: T) => R;type StringHandler = Handler<string, void>;
type은 원시 타입, 유니온, 인터섹션, 튜플, 함수 타입 등 무엇이든 이름을 붙일 수 있습니다.
interface 기본
// 파일: src/interface-vs-type.tsinterface User { id: number; name: string; email: string;}// interface는 객체 타입만 정의 가능// interface Status = "active" | "inactive"; // Error — 유니온은 type만 가능
핵심 차이 1 — 선언 병합 (Declaration Merging)
interface는 같은 이름으로 여러 번 선언하면 자동으로 병합됩니다. type은 불가능합니다.
// 파일: src/declaration-merging.tsinterface Window { myPlugin: string;}interface Window { anotherPlugin: number;}// 실제 Window는 두 선언이 합쳐진 것const w: Window = { // ... 브라우저 기본 프로퍼티들 ... myPlugin: "active", anotherPlugin: 42,};
이 기능 덕분에 전역 타입 확장, 라이브러리 타입 보강(ambient declaration)이 가능합니다.
// 파일: src/ambient-extend.ts// Express Request에 userId 추가declare namespace Express { interface Request { userId?: number; }}// 이후 req.userId로 접근 가능
핵심 차이 2 — 타입 표현 범위
// 파일: src/type-only-features.ts// type만 가능한 것들// 1. 유니온type Result = "ok" | "error";// 2. 인터섹션 (interface는 extends로 대체 가능하지만 type이 더 간결할 때가 있음)type Combined = TypeA & TypeB & TypeC;// 3. 튜플type Pair<T, U> = [T, U];// 4. 조건부 타입 (PART 04에서 다룸)type IsString<T> = T extends string ? true : false;// 5. 원시 타입 별칭type ID = string | number;// 6. typeof / keyof 활용type Keys = keyof SomeObject;type Config = typeof defaultConfig;
핵심 차이 3 — 에러 메시지
interface는 에러 메시지에서 이름이 표시됩니다. type의 복잡한 표현식은 인라인으로 풀어서 보여줄 수 있습니다.
// interface: 에러 메시지에 'User' 타입으로 표시됨interface User { id: number; name: string }// type 별칭: 복잡한 타입은 에러 메시지에 풀어서 표시됨type User = { id: number; name: string }
실무에서는 차이가 크지 않지만, IDE 툴팁 가독성에서 interface가 더 깔끔한 경우가 있습니다.
실무 선택 기준
// 파일: src/choosing-guide.ts// interface를 선택하는 경우// 1. 클래스가 구현(implements)해야 하는 계약interface Repository<T> { findById(id: number): Promise<T | null>; save(entity: T): Promise<T>; delete(id: number): Promise<void>;}class UserRepository implements Repository<User> { async findById(id: number) { /* ... */ return null; } async save(user: User) { /* ... */ return user; } async delete(id: number) { /* ... */ }}// 2. 외부에 공개되는 API 타입 (라이브러리 작성 시 확장 가능)export interface PluginOptions { timeout?: number; retries?: number;}
// type을 선택하는 경우// 1. 유니온/인터섹션 조합type ApiResponse<T> = { data: T; meta: ApiMeta } | { error: string };// 2. 유틸리티 타입 활용type ReadonlyUser = Readonly<User>;type PartialConfig = Partial<Config>;// 3. 함수 타입type EventHandler = (event: Event) => void;// 4. 튜플 타입type Coordinate = [number, number];// 5. 재귀 타입type JSONValue = | string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
요약표
| 기능 | type |
interface |
|---|---|---|
| 객체 타입 정의 | O | O |
| 유니온 타입 | O | X |
| 인터섹션 타입 | O | △ (extends로 유사하게) |
| 튜플 타입 | O | X |
| 함수 타입 | O | O |
| 선언 병합 | X | O |
implements (클래스) |
O | O |
extends (확장) |
O | O |
| 조건부 타입 | O | X |
실무 팀 컨벤션 예시
대부분의 팀은 아래 규칙 중 하나를 씁니다.
Option A — interface 우선
- 객체 타입은
interface, 나머지(유니온, 함수 등)는type - React 컴포넌트 props도
interface Props로 통일
Option B — type 우선
- 모든 타입 정의에
type사용 interface는 클래스 계약과 선언 병합이 필요한 곳만
둘 중 무엇을 선택하든 팀 내에서 일관성이 가장 중요합니다. TypeScript 공식 문서는 "확장 가능성이 필요하면 interface, 그 외엔 type"을 권장하지만, 강제 사항은 아닙니다.