목차
interface와 type의 차이 제대로 이해하기
지난 시간에는 객체의 구조를 타입으로 표현하고, 반복되는 구조에 `type` 별칭으로 이름을 붙이는 방법을 배웠습니다.
type User = {
readonly id: number;
name: string;
email: string;
nickname?: string;
};이제 `User`라는 설계도를 여러 곳에서 재사용할 수 있습니다.
const user: User = {
id: 1,
name: "김타입",
email: "type@example.com",
};그런데 TypeScript에는 객체 설계도를 만드는 또 다른 문법이 있습니다.
interface User {
readonly id: number;
name: string;
email: string;
nickname?: string;
}바로 `interface`입니다.
처음 보면 `type`과 거의 똑같아 보입니다.
type Product = {
id: number;
name: string;
price: number;
};interface Product {
id: number;
name: string;
price: number;
}두 설계도 모두 다음 상품 객체를 검사할 수 있습니다.
const product: Product = {
id: 1,
name: "기계식 키보드",
price: 120000,
};그렇다면 이런 의문이 생깁니다.
둘이 같은 기능이라면 왜 문법이 두 개일까?
type만 사용하면 안 될까?
interface만 사용하면 안 될까?
실무에서는 무엇을 선택해야 할까?두 문법은 상당히 비슷하지만 완전히 같지는 않습니다.
`type`이 여러 종류의 타입을 자유롭게 조립하는 종합 공구함이라면, `interface`는 객체 구조를 확장하고 협업하는 데 특화된 조립식 설계도입니다. 🧰
오늘은 이 두 설계도를 나란히 펼쳐놓고 어디까지 같고, 어디에서 갈라지는지 살펴보겠습니다.
1. 이번 시간에 배울 내용
이번 시간에는 다음 내용을 알아봅니다.
- `interface`는 무엇인가?
- 인터페이스의 기본 문법
- 선택적 속성과 읽기 전용 속성
- 메서드를 가진 인터페이스
- 함수 구조를 인터페이스로 표현하는 방법
- 객체 배열과 함수에서 인터페이스 사용하기
- `extends`로 인터페이스 확장하기
- 여러 인터페이스를 동시에 확장하기
- 인터페이스 확장 중 속성이 충돌하면 어떻게 되는가?
- 선언 병합은 무엇인가?
- `type`과 `interface`의 공통점
- `type`에서만 자연스럽게 표현할 수 있는 타입
- `interface`와 교차 타입의 차이
- 인터페이스와 클래스의 관계
- 구조적 타이핑과 인터페이스
- 실무에서 `type`과 `interface`를 선택하는 기준
이번 시간의 핵심 질문은 하나입니다.
객체 설계도를 만들 때 `type`과 `interface` 중 무엇을 선택해야 할까?
정답부터 살짝 공개하면 둘 중 하나가 무조건 정답인 것은 아닙니다.
설계도의 목적과 프로젝트 규칙에 따라 선택할 수 있습니다.
2. `interface`란?
`interface`는 객체가 가져야 할 구조를 정의하는 TypeScript 문법입니다.
interface User {
id: number;
name: string;
email: string;
}`User` 인터페이스는 다음과 같은 객체 구조를 요구합니다.
id
→ number
name
→ string
email
→ string이 인터페이스를 객체의 타입으로 사용할 수 있습니다.
const user: User = {
id: 1,
name: "김타입",
email: "type@example.com",
};인터페이스가 요구하는 속성이 빠지면 오류가 발생합니다.
const user: User = {
id: 1,
name: "김타입",
};`email`이 없기 때문입니다.
속성 타입이 달라도 오류입니다.
const user: User = {
id: "1",
name: "김타입",
email: "type@example.com",
};`id`는 `number`여야 하지만 문자열이 들어갔습니다.
인터페이스는 객체를 직접 만들지는 않습니다.
객체가 어떤 규칙을 따라야 하는지 설명하는 계약서 역할을 합니다.
3. 인터페이스 기본 문법
인터페이스는 다음과 같은 형식으로 작성합니다.
interface 인터페이스이름 {
속성이름: 타입;
속성이름: 타입;
}상품 인터페이스를 만들어 보겠습니다.
interface Product {
id: number;
name: string;
price: number;
isSoldOut: boolean;
}상품 객체를 생성합니다.
const keyboard: Product = {
id: 1,
name: "기계식 키보드",
price: 120000,
isSoldOut: false,
};다른 상품에도 같은 인터페이스를 사용할 수 있습니다.
const mouse: Product = {
id: 2,
name: "무선 마우스",
price: 60000,
isSoldOut: true,
};하나의 설계도를 여러 상품 객체가 공유합니다.
Product 설계도
├─ keyboard
├─ mouse
└─ monitor상품이 늘어나도 설계도를 복사할 필요가 없습니다.
4. 인터페이스 이름 작성하기
인터페이스 이름은 보통 파스칼 표기법으로 작성합니다.
interface User {
id: number;
}interface ProductItem {
id: number;
name: string;
}interface ShoppingCart {
items: ProductItem[];
}파스칼 표기법은 각 단어의 첫 글자를 대문자로 작성합니다.
User
ProductItem
ShoppingCart
RequestOptions예전 코드에서는 인터페이스 이름 앞에 `I`를 붙이는 방식을 볼 수도 있습니다.
interface IUser {
id: number;
name: string;
}하지만 TypeScript에서는 다음처럼 별도의 접두어 없이 의미 있는 이름을 사용하는 경우가 많습니다.
interface User {
id: number;
name: string;
}프로젝트에 이미 정해진 규칙이 있다면 그 규칙을 따르는 것이 가장 중요합니다.
코딩 규칙은 팀의 교통법규입니다.
혼자만 우측통행을 외치며 좌측으로 달리면 사고가 납니다. 🚦
5. 선택적 속성 사용하기
인터페이스에서도 선택적 속성 `?`를 사용할 수 있습니다.
interface User {
id: number;
name: string;
nickname?: string;
}`nickname`은 생략할 수 있습니다.
const firstUser: User = {
id: 1,
name: "김타입",
};닉네임을 포함해도 됩니다.
const secondUser: User = {
id: 2,
name: "이컴파일",
nickname: "컴파일러",
};선택적 속성을 읽으면 `undefined`일 가능성이 있습니다.
console.log(
secondUser.nickname?.toUpperCase()
);기본값을 사용할 수도 있습니다.
const displayName =
firstUser.nickname
?? firstUser.name;`interface`에서도 `type`과 동일하게 선택적 속성을 표현할 수 있습니다.
6. 읽기 전용 속성 사용하기
`readonly` 속성도 사용할 수 있습니다.
interface User {
readonly id: number;
name: string;
email: string;
}객체를 생성할 때 `id`를 작성합니다.
const user: User = {
id: 1,
name: "김타입",
email: "type@example.com",
};일반 속성은 변경할 수 있습니다.
user.name = "이타입";
user.email = "new@example.com";하지만 읽기 전용 속성은 변경할 수 없습니다.
user.id = 2;TypeScript가 오류를 표시합니다.
Cannot assign to 'id'
because it is a read-only property.`readonly`는 타입 검사 단계에서 재할당을 막습니다.
실행 중 객체를 자동으로 동결하는 기능은 아닙니다.
7. 선택적 속성과 읽기 전용 속성 함께 사용하기
두 기능을 함께 사용할 수도 있습니다.
interface User {
readonly id: number;
name: string;
nickname?: string;
profileImage?: string;
}실제 객체를 만들어 보겠습니다.
const user: User = {
id: 1,
name: "김타입",
nickname: "타입마스터",
};다음 규칙이 적용됩니다.
id
→ 반드시 필요
→ number
→ 생성 후 수정 불가
name
→ 반드시 필요
→ string
→ 수정 가능
nickname
→ 생략 가능
→ 존재한다면 string
profileImage
→ 생략 가능
→ 존재한다면 string인터페이스만 읽어도 객체의 사용 규칙을 파악할 수 있습니다.
좋은 인터페이스는 사용 설명서와 설계도를 한 장에 겹쳐놓은 문서입니다.
8. 인터페이스에 메서드 정의하기
객체에는 데이터뿐 아니라 메서드도 포함할 수 있습니다.
interface User {
name: string;
greet(): string;
}객체를 만들어 보겠습니다.
const user: User = {
name: "김타입",
greet() {
return `안녕하세요, ${this.name}입니다.`;
},
};메서드를 호출합니다.
console.log(user.greet());출력 결과:
안녕하세요, 김타입입니다.매개변수가 있는 메서드도 정의할 수 있습니다.
interface Calculator {
add(
firstNumber: number,
secondNumber: number
): number;
}const calculator: Calculator = {
add(firstNumber, secondNumber) {
return firstNumber + secondNumber;
},
};console.log(
calculator.add(10, 20)
);출력 결과:
309. 함수 속성 문법 사용하기
메서드를 함수 속성 형태로 작성할 수도 있습니다.
interface User {
name: string;
greet: (
message: string
) => string;
}객체를 구현합니다.
const user: User = {
name: "김타입",
greet(message) {
return `${message}, ${this.name}입니다.`;
},
};두 문법을 비교해 보겠습니다.
메서드 문법
interface User {
greet(message: string): string;
}함수 속성 문법
interface User {
greet: (
message: string
) => string;
}기본적인 사용에서는 비슷하게 느껴질 수 있습니다.
프로젝트의 스타일과 함수 속성을 다루는 목적에 맞춰 선택할 수 있습니다.
10. 선택적 메서드
메서드도 선택 사항으로 만들 수 있습니다.
interface DialogOptions {
title: string;
message: string;
onConfirm?: () => void;
onCancel?: () => void;
}확인 함수만 가진 객체를 만들 수 있습니다.
const options: DialogOptions = {
title: "삭제 확인",
message: "정말 삭제하시겠습니까?",
onConfirm() {
console.log("삭제합니다.");
},
};선택적 메서드는 존재 여부를 확인한 뒤 호출해야 합니다.
options.onConfirm?.();
options.onCancel?.();다이얼로그마다 모든 버튼 동작을 억지로 작성할 필요가 없습니다.
취소 버튼이 없는 창에 빈 취소 함수를 세워두는 것도 조금 쓸쓸한 풍경입니다.
11. 인터페이스를 함수 매개변수에 사용하기
사용자 정보를 출력하는 함수를 만들어 보겠습니다.
interface User {
id: number;
name: string;
email: string;
}function printUser(
user: User
): void {
console.log(`번호: ${user.id}`);
console.log(`이름: ${user.name}`);
console.log(`이메일: ${user.email}`);
}호출할 때 객체 구조를 검사받습니다.
printUser({
id: 1,
name: "김타입",
email: "type@example.com",
});잘못된 객체는 전달할 수 없습니다.
printUser({
id: 1,
name: "김타입",
});`email`이 누락되었습니다.
인터페이스는 함수가 어떤 객체를 받을 수 있는지 명확하게 보여줍니다.
12. 함수 반환 타입에 인터페이스 사용하기
함수가 객체를 반환할 때도 인터페이스를 사용할 수 있습니다.
interface User {
id: number;
name: string;
email: string;
}function createUser(
id: number,
name: string,
email: string
): User {
return {
id,
name,
email,
};
}함수를 호출합니다.
const user = createUser(
1,
"김타입",
"type@example.com"
);반환 객체에 속성이 빠지면 오류가 발생합니다.
function createUser(): User {
return {
id: 1,
name: "김타입",
};
}`email`이 없기 때문입니다.
인터페이스가 출고되는 객체의 부품 목록을 검사합니다. 🏭
13. 인터페이스 배열 만들기
같은 구조의 객체를 여러 개 관리할 수 있습니다.
interface User {
id: number;
name: string;
isActive: boolean;
}const users: User[] = [
{
id: 1,
name: "김타입",
isActive: true,
},
{
id: 2,
name: "이컴파일",
isActive: false,
},
{
id: 3,
name: "박제네릭",
isActive: true,
},
];활성 사용자만 선택해 보겠습니다.
const activeUsers =
users.filter(
(user) => user.isActive
);사용자 이름만 추출합니다.
const activeUserNames =
activeUsers.map(
(user) => user.name
);인터페이스가 정의되어 있으므로 배열 메서드 안에서도 속성 타입을 자동으로 알 수 있습니다.
14. 중첩 인터페이스
인터페이스의 속성으로 다른 인터페이스를 사용할 수 있습니다.
interface Address {
city: string;
district: string;
detail?: string;
}interface User {
id: number;
name: string;
address: Address;
}객체를 생성합니다.
const user: User = {
id: 1,
name: "김타입",
address: {
city: "서울",
district: "강남구",
},
};중첩 속성에 접근합니다.
console.log(
user.address.city
);주소 인터페이스는 다른 객체에서도 재사용할 수 있습니다.
interface Delivery {
orderId: string;
recipient: string;
address: Address;
}객체 구조가 커질수록 작은 인터페이스로 분리해 조합하는 편이 읽기 좋습니다.
큰 건물도 방 하나하나의 도면부터 시작합니다.
15. 인터페이스로 함수 구조 표현하기
인터페이스는 객체뿐 아니라 호출 가능한 함수 구조도 표현할 수 있습니다.
interface Calculator {
(
firstNumber: number,
secondNumber: number
): number;
}함수를 할당합니다.
const add: Calculator = (
firstNumber,
secondNumber
) => {
return firstNumber + secondNumber;
};console.log(
add(10, 20)
);출력 결과:
30함수에 속성까지 붙은 구조도 표현할 수 있습니다.
interface Counter {
(): number;
count: number;
}이런 고급 구조는 일반 애플리케이션 코드에서는 자주 필요하지 않을 수 있습니다.
인터페이스가 단순히 객체 리터럴 전용 문법은 아니라는 점만 기억해 두면 충분합니다.
16. 인터페이스 확장이란?
인터페이스의 대표적인 장점 중 하나는 기존 인터페이스를 확장할 수 있다는 것입니다.
기본 사용자 인터페이스를 만들어 보겠습니다.
interface User {
id: number;
name: string;
email: string;
}관리자는 일반 사용자 정보와 추가 권한을 가집니다.
interface AdminUser
extends User {
permissions: string[];
}`AdminUser`는 `User`의 모든 속성과 `permissions`를 가져야 합니다.
const admin: AdminUser = {
id: 1,
name: "김관리자",
email: "admin@example.com",
permissions: [
"user:read",
"user:write",
"user:delete",
],
};`extends`는 기존 설계도를 복사하는 것이 아니라 연결해서 확장합니다.
User
├─ id
├─ name
└─ email
AdminUser extends User
├─ id
├─ name
├─ email
└─ permissions기존 건물 위에 관리자 전용 통제실을 한 층 추가한 셈입니다. 🏢
17. 확장된 인터페이스의 필수 속성
확장된 인터페이스는 부모 인터페이스의 모든 필수 속성을 포함해야 합니다.
interface User {
id: number;
name: string;
}interface AdminUser
extends User {
permissions: string[];
}다음 객체에는 `name`이 없습니다.
const admin: AdminUser = {
id: 1,
permissions: [
"user:read",
],
};오류가 발생합니다.
`AdminUser`가 직접 선언한 속성만 작성하면 되는 것이 아닙니다.
`User`에서 물려받은 속성도 모두 지켜야 합니다.
18. 여러 단계로 확장하기
인터페이스는 여러 단계로 이어서 확장할 수 있습니다.
interface Entity {
readonly id: number;
}interface User
extends Entity {
name: string;
email: string;
}interface AdminUser
extends User {
permissions: string[];
}`AdminUser`에는 세 단계의 속성이 모두 포함됩니다.
const admin: AdminUser = {
id: 1,
name: "김관리자",
email: "admin@example.com",
permissions: [
"settings:write",
],
};구조를 그림으로 표현하면 다음과 같습니다.
Entity
└─ id
User extends Entity
├─ id
├─ name
└─ email
AdminUser extends User
├─ id
├─ name
├─ email
└─ permissions확장이 깊어질수록 각 단계의 역할이 명확해야 합니다.
상속 계보가 너무 길어지면 새 개발자가 타입 족보를 들고 조상 찾기에 나서야 합니다. 📜
19. 여러 인터페이스를 동시에 확장하기
하나의 인터페이스가 여러 인터페이스를 동시에 확장할 수도 있습니다.
interface Identifiable {
readonly id: number;
}interface Timestamped {
createdAt: string;
updatedAt: string;
}interface Named {
name: string;
}세 인터페이스를 모두 확장합니다.
interface Product
extends
Identifiable,
Timestamped,
Named {
price: number;
}상품 객체는 모든 속성을 포함해야 합니다.
const product: Product = {
id: 1,
name: "기계식 키보드",
price: 120000,
createdAt:
"2026-08-02",
updatedAt:
"2026-08-02",
};이를 다중 확장이라고 할 수 있습니다.
Identifiable ─┐
Timestamped ─┼─→ Product
Named ─┘필요한 설계도 조각을 여러 장 가져와 하나의 객체 계약으로 조립합니다.
20. 다중 확장의 장점
공통 속성을 작은 인터페이스로 분리하면 여러 객체에서 재사용할 수 있습니다.
interface Identifiable {
readonly id: number;
}interface Timestamped {
createdAt: string;
updatedAt: string;
}사용자 인터페이스입니다.
interface User
extends
Identifiable,
Timestamped {
name: string;
email: string;
}게시글 인터페이스입니다.
interface Post
extends
Identifiable,
Timestamped {
title: string;
content: string;
}상품 인터페이스입니다.
interface Product
extends
Identifiable,
Timestamped {
name: string;
price: number;
}중복되는 속성을 매번 다시 작성할 필요가 없습니다.
공통 조각
├─ Identifiable
└─ Timestamped
조립 결과
├─ User
├─ Post
└─ Product21. 인터페이스 확장 중 속성 재정의하기
부모 인터페이스의 속성을 더 구체적인 타입으로 재정의할 수 있는 경우가 있습니다.
interface User {
id: string | number;
role: string;
}관리자 타입에서 `role`을 더 구체적으로 제한해 보겠습니다.
interface AdminUser
extends User {
role: "admin";
permissions: string[];
}`"admin"`은 `string`에 포함되는 더 구체적인 타입입니다.
따라서 다음 객체를 만들 수 있습니다.
const admin: AdminUser = {
id: 1,
role: "admin",
permissions: [
"user:read",
],
};하지만 서로 호환되지 않는 타입으로 바꾸면 오류가 발생합니다.
interface WrongAdmin
extends User {
id: boolean;
}부모의 `id`는 `string | number`인데 자식에서 `boolean`으로 바꿨습니다.
부모 계약과 호환되지 않기 때문에 확장할 수 없습니다.
증축 공사를 한다고 건물의 기둥을 젤리로 바꿀 수는 없습니다.
22. 다중 확장에서 속성이 충돌하면?
두 인터페이스에 같은 이름의 속성이 있지만 타입이 다르다고 가정해 보겠습니다.
interface StringId {
id: string;
}interface NumberId {
id: number;
}두 인터페이스를 동시에 확장합니다.
interface User
extends
StringId,
NumberId {
name: string;
}오류가 발생합니다.
`id`가 문자열이어야 하는지 숫자여야 하는지 결정할 수 없기 때문입니다.
StringId.id
→ string
NumberId.id
→ number
User.id
→ 어느 쪽인가?다중 확장을 사용할 때는 같은 이름의 속성이 서로 호환되는지 확인해야 합니다.
두 설계팀이 같은 방을 한쪽은 주방, 다른 쪽은 수영장으로 지정하면 시공팀이 회의를 중단합니다.
23. `type`에서 객체 타입 확장하기
`type` 별칭에는 `extends`를 사용하지 않습니다.
대신 교차 타입 `&`로 객체 타입을 결합합니다.
type User = {
id: number;
name: string;
email: string;
};type AdminUser =
User & {
permissions: string[];
};객체를 생성합니다.
const admin: AdminUser = {
id: 1,
name: "김관리자",
email: "admin@example.com",
permissions: [
"user:read",
],
};두 방식을 비교하면 다음과 같습니다.
`interface`
interface AdminUser
extends User {
permissions: string[];
}`type`
type AdminUser =
User & {
permissions: string[];
};결과적으로 기존 객체 타입에 새로운 속성을 조합할 수 있습니다.
24. `interface`와 `type`은 서로 조합할 수 있다
둘 중 하나만 사용해야 하는 것은 아닙니다.
인터페이스를 타입 별칭에서 사용할 수 있습니다.
interface User {
id: number;
name: string;
}type AdminUser =
User & {
permissions: string[];
};타입 별칭을 인터페이스가 확장할 수도 있습니다.
type BasicUser = {
id: number;
name: string;
};interface AdminUser
extends BasicUser {
permissions: string[];
}단, 인터페이스가 확장하려는 타입은 정적으로 알 수 있는 객체 구조여야 합니다.
예를 들어 객체가 아닌 단순 유니언 타입을 그대로 확장할 수는 없습니다.
type Identifier =
string | number;interface User
extends Identifier {
name: string;
}이런 구조는 적절하지 않습니다.
`interface`는 기본적으로 객체 구조를 확장하는 데 초점을 둡니다.
25. 선언 병합이란?
`interface`의 가장 독특한 기능 중 하나는 선언 병합(Declaration Merging)입니다.
같은 이름의 인터페이스를 여러 번 선언해 보겠습니다.
interface User {
id: number;
}interface User {
name: string;
}TypeScript는 두 선언을 합칩니다.
결과적으로 `User`는 다음과 같은 구조가 됩니다.
interface User {
id: number;
name: string;
}따라서 객체에는 두 속성이 모두 필요합니다.
const user: User = {
id: 1,
name: "김타입",
};`id`만 작성하면 오류입니다.
const user: User = {
id: 1,
};두 번째 선언의 `name`도 병합되었기 때문입니다.
인터페이스는 같은 이름의 설계도가 추가로 도착하면 기존 설계도 뒤에 새 도면을 붙입니다. 📎
26. `type` 별칭은 선언 병합이 되지 않는다
같은 이름의 타입 별칭을 두 번 선언할 수 없습니다.
type User = {
id: number;
};type User = {
name: string;
};오류가 발생합니다.
Duplicate identifier 'User'.`type` 별칭은 한 번 정의한 이름을 다시 열어서 속성을 추가할 수 없습니다.
확장하려면 새로운 타입을 만들어야 합니다.
type BasicUser = {
id: number;
};type User =
BasicUser & {
name: string;
};정리하면 다음과 같습니다.
interface
같은 이름으로 다시 선언 가능
선언 내용이 병합됨
type
같은 이름으로 다시 선언 불가
새 타입을 만들어 조합해야 함27. 선언 병합이 유용한 상황
선언 병합은 기존 라이브러리의 타입을 확장할 때 유용할 수 있습니다.
브라우저의 `Window` 인터페이스에 애플리케이션 전용 속성을 추가하는 예를 살펴보겠습니다.
interface Window {
appVersion: string;
}이제 브라우저 환경에서 다음 속성을 사용할 수 있도록 타입을 확장할 수 있습니다.
window.appVersion = "1.0.0";
console.log(
window.appVersion
);외부 라이브러리나 전역 객체 타입을 보강하는 것을 타입 선언 확장 또는 모듈 보강과 연결해서 설명하기도 합니다.
다만 무분별하게 전역 인터페이스를 병합하면 예상하지 못한 충돌이 발생할 수 있습니다.
공용 게시판에 누구나 설계도를 붙일 수 있다고 해서 모든 사람이 동시에 벽을 옮겨도 되는 것은 아닙니다.
28. 선언 병합 시 같은 속성 타입이 다르면?
같은 이름의 인터페이스에 같은 속성을 다시 선언할 때는 타입이 호환되어야 합니다.
interface User {
id: number;
}interface User {
id: number;
name: string;
}`id` 타입이 같으므로 병합할 수 있습니다.
하지만 다음은 문제가 됩니다.
interface Product {
id: number;
}interface Product {
id: string;
}같은 `id` 속성의 타입이 서로 다릅니다.
TypeScript는 하나의 속성이 동시에 `number`와 `string`이라고 결정할 수 없으므로 오류를 표시합니다.
선언 병합은 종이를 그냥 포개는 기능이 아닙니다.
겹치는 부분의 규칙도 서로 일치해야 합니다.
29. `type`과 `interface`의 공통점
두 문법은 객체 타입을 정의할 때 매우 비슷한 기능을 제공합니다.
객체 구조 정의
type TypeUser = {
id: number;
name: string;
};interface InterfaceUser {
id: number;
name: string;
}선택적 속성
type TypeUser = {
nickname?: string;
};interface InterfaceUser {
nickname?: string;
}읽기 전용 속성
type TypeUser = {
readonly id: number;
};interface InterfaceUser {
readonly id: number;
}메서드
type TypeUser = {
greet(): string;
};interface InterfaceUser {
greet(): string;
}함수 매개변수와 반환 타입에 사용
function printUser(
user: InterfaceUser
): void {}두 문법 모두 객체의 구조를 안전하게 표현할 수 있습니다.
30. `type`은 객체 외의 타입에도 사용할 수 있다
`type` 별칭은 객체뿐 아니라 다양한 타입 표현에 이름을 붙일 수 있습니다.
기본 타입 별칭
type UserId = number;유니언 타입
type Status =
| "idle"
| "loading"
| "success"
| "error";튜플
type Coordinate = [
x: number,
y: number,
];함수 타입
type Calculator = (
firstNumber: number,
secondNumber: number
) => number;객체 타입
type User = {
id: number;
name: string;
};`type`은 타입 표현 전체에 이름을 붙이는 범용 도구입니다.
31. `interface`는 객체 구조에 특화되어 있다
`interface`는 주로 객체, 클래스 계약, 함수의 호출 구조처럼 멤버를 가진 구조를 표현합니다.
다음처럼 기본 타입 하나의 별칭을 직접 만들지는 않습니다.
interface UserId = number;이런 문법은 사용할 수 없습니다.
유니언 타입도 인터페이스 자체로 직접 선언할 수 없습니다.
interface Status =
| "idle"
| "loading";튜플 별칭에도 `type`이 더 자연스럽습니다.
type Coordinate = [
number,
number,
];정리하면 다음과 같습니다.
객체 구조 중심
→ interface 또는 type
유니언, 튜플, 기본 타입 별칭
→ type32. `type`과 `interface` 비교표
| 항목 | `type` | `interface` |
|---|---|---|
| 객체 타입 정의 | 가능 | 가능 |
| 선택적 속성 | 가능 | 가능 |
| `readonly` 속성 | 가능 | 가능 |
| 메서드 정의 | 가능 | 가능 |
| 유니언 타입 | 가능 | 직접 선언 불가 |
| 튜플 타입 | 가능 | 일반적으로 `type` 사용 |
| 기본 타입 별칭 | 가능 | 불가 |
| 객체 타입 확장 | `&` 사용 | `extends` 사용 |
| 선언 병합 | 불가 | 가능 |
| 클래스 계약 | 가능하지만 보통 `interface`가 자연스러움 | 가능 |
| 다시 열어 확장 | 불가 | 가능 |
두 문법의 차이는 우열보다 성격의 차이에 가깝습니다.
33. 구조적 타이핑은 두 문법 모두 동일하다
TypeScript는 타입 이름보다 객체의 실제 구조를 확인합니다.
interface User {
id: number;
name: string;
}const developer = {
id: 1,
name: "김타입",
language: "TypeScript",
};함수에 전달할 수 있습니다.
function printUser(
user: User
): void {
console.log(user.name);
}printUser(developer);`developer`가 `User` 인터페이스의 필수 구조를 모두 가지고 있기 때문입니다.
`type` 별칭을 사용해도 같은 원리가 적용됩니다.
type User = {
id: number;
name: string;
};구조적 타이핑은 `type`과 `interface`를 차별하지 않습니다.
입장 심사에서 설계도 브랜드가 아니라 실제 속성 목록을 확인합니다.
34. 인터페이스와 초과 속성 검사
객체 리터럴을 직접 전달하면 초과 속성 검사가 적용됩니다.
interface User {
id: number;
name: string;
}function printUser(
user: User
): void {
console.log(user.name);
}다음 객체 리터럴에는 `role`이 추가되어 있습니다.
printUser({
id: 1,
name: "김타입",
role: "admin",
});`User` 인터페이스에 `role`이 없으므로 오류가 발생할 수 있습니다.
하지만 변수에 먼저 저장하면 구조적 타이핑으로 전달할 수 있습니다.
const admin = {
id: 1,
name: "김타입",
role: "admin",
};
printUser(admin);이 동작은 `type` 별칭에서도 동일합니다.
초과 속성 검사는 `interface`만의 기능이 아닙니다.
35. 인터페이스와 클래스
인터페이스는 클래스가 구현해야 할 구조를 정의할 수 있습니다.
interface Printable {
print(): void;
}클래스에서 `implements`를 사용합니다.
class Document
implements Printable {
print(): void {
console.log(
"문서를 출력합니다."
);
}
}객체를 생성합니다.
const document =
new Document();
document.print();클래스에 필요한 메서드가 없으면 오류가 발생합니다.
class Image
implements Printable {
}`print()` 메서드가 없기 때문입니다.
인터페이스가 클래스에 다음과 같은 계약서를 전달한 셈입니다.
Printable로 등록하려면 반드시 print 기능을 준비하세요.
36. 여러 인터페이스 구현하기
클래스는 여러 인터페이스를 구현할 수 있습니다.
interface Printable {
print(): void;
}interface Downloadable {
download(): void;
}class Report
implements
Printable,
Downloadable {
print(): void {
console.log(
"보고서를 출력합니다."
);
}
download(): void {
console.log(
"보고서를 다운로드합니다."
);
}
}클래스는 두 계약을 모두 지켜야 합니다.
Printable
→ print 필요
Downloadable
→ download 필요
Report
→ print와 download 모두 구현클래스는 이후 별도 회차에서 더 자세히 다룰 수 있습니다.
이번에는 인터페이스가 객체뿐 아니라 클래스가 따라야 할 기능 계약도 표현할 수 있다는 점만 기억하면 됩니다.
37. 인터페이스는 구현 코드를 제공하지 않는다
다음 인터페이스는 메서드 구조만 정의합니다.
interface Logger {
log(message: string): void;
}인터페이스 안에는 실제 구현 코드가 없습니다.
interface Logger {
log(message: string): void {
console.log(message);
}
}이런 방식으로 메서드 본문을 작성할 수는 없습니다.
실제 동작은 객체나 클래스에서 구현합니다.
const consoleLogger: Logger = {
log(message) {
console.log(
`[LOG] ${message}`
);
},
};인터페이스는 “무엇을 해야 하는가”를 정의합니다.
실제 객체는 “어떻게 할 것인가”를 구현합니다.
interface
→ 기능 목록
객체 또는 클래스
→ 실제 기능 코드메뉴판이 요리 방법까지 대신 수행하지는 않습니다. 🍳
38. 인터페이스를 반환하는 팩토리 함수
인터페이스 구조를 만족하는 객체를 함수에서 만들 수 있습니다.
interface Counter {
count: number;
increase(): void;
decrease(): void;
}function createCounter(): Counter {
return {
count: 0,
increase() {
this.count += 1;
},
decrease() {
this.count -= 1;
},
};
}사용합니다.
const counter =
createCounter();
counter.increase();
counter.increase();
counter.decrease();
console.log(counter.count);출력 결과:
1반환 타입을 `Counter`로 지정했기 때문에 함수가 필요한 구조를 모두 반환하는지 검사받을 수 있습니다.
39. 인터페이스 확장과 교차 타입의 읽기 차이
두 방식 모두 객체 타입을 조합할 수 있습니다.
인터페이스 확장
interface User {
id: number;
name: string;
}interface AdminUser
extends User {
permissions: string[];
}문장처럼 읽을 수 있습니다.
AdminUser는 User를 확장한다.교차 타입
type User = {
id: number;
name: string;
};type AdminUser =
User & {
permissions: string[];
};다음처럼 읽을 수 있습니다.
AdminUser는 User와
permissions 구조를 모두 만족한다.객체 계층 관계가 명확하다면 `extends`가 자연스러울 수 있습니다.
서로 독립된 타입 조각을 조합한다면 `&`가 자연스러울 수 있습니다.
40. 속성 충돌에서 교차 타입이 만드는 결과
교차 타입에서 같은 이름의 속성이 서로 다른 타입을 가지면 매우 까다로운 결과가 생길 수 있습니다.
type StringId = {
id: string;
};type NumberId = {
id: number;
};두 타입을 교차합니다.
type ImpossibleId =
StringId &
NumberId;`id`는 문자열이면서 동시에 숫자여야 합니다.
개념적으로 다음과 같은 타입이 됩니다.
id: string & number문자열이면서 숫자인 일반적인 값은 존재하지 않으므로 `never`와 비슷한 불가능한 속성이 만들어질 수 있습니다.
const item: ImpossibleId = {
id: 1,
};오류가 발생합니다.
인터페이스의 `extends`는 충돌을 확장 시점에 비교적 명확하게 알려줍니다.
교차 타입은 결합된 뒤 불가능한 타입을 만들어낼 수 있으므로 속성 충돌을 주의해야 합니다.
두 퍼즐 조각을 힘으로 누른다고 맞지 않는 홈이 사라지지는 않습니다. 🧩
41. 인터페이스를 확장할 때 이름을 명확히 만들기
확장 관계가 이름에서도 드러나면 타입을 이해하기 쉽습니다.
interface User {
id: number;
name: string;
}interface AdminUser
extends User {
permissions: string[];
}interface GuestUser
extends User {
expiresAt: string;
}반면 의미가 모호한 이름은 구조를 이해하기 어렵게 만듭니다.
interface User2
extends User {
permissions: string[];
}interface AdvancedUser
extends User {
expiresAt: string;
}타입 이름은 속성 목록만큼 중요합니다.
좋은 이름은 타입 정의를 열어보지 않아도 역할을 짐작하게 해줍니다.
42. 지나친 상속보다 조합을 고려하자
인터페이스 확장은 편리하지만 상속 단계가 지나치게 깊어지면 구조를 파악하기 어려워집니다.
Entity
↓
Person
↓
User
↓
PaidUser
↓
PremiumUser
↓
EnterprisePremiumUser마지막 인터페이스가 어떤 속성을 가지고 있는지 확인하려면 여러 파일을 오가야 할 수 있습니다.
작은 기능 인터페이스를 조합하는 방식도 고려할 수 있습니다.
interface Identifiable {
readonly id: number;
}interface Named {
name: string;
}interface HasEmail {
email: string;
}interface User
extends
Identifiable,
Named,
HasEmail {
isActive: boolean;
}무조건 상속 단계를 늘리기보다 의미 있는 조각을 조합하는 편이 관리하기 쉬운 경우가 많습니다.
타입 가계도가 왕실 족보처럼 길어지기 전에 구조를 다시 살펴봐야 합니다.
43. `type`을 사용하기 좋은 상황
다음과 같은 경우에는 `type` 별칭이 자연스럽습니다.
유니언 타입
type RequestStatus =
| "idle"
| "loading"
| "success"
| "error";튜플 타입
type Coordinate = [
x: number,
y: number,
];함수 타입
type ClickHandler =
(event: MouseEvent) => void;객체와 다른 타입의 조합
type ApiResult =
| {
success: true;
data: User;
}
| {
success: false;
error: string;
};교차 타입
type AdminUser =
User &
PermissionInfo;`type`은 여러 타입 재료를 섞어 새로운 타입을 만드는 데 유연합니다.
44. `interface`를 사용하기 좋은 상황
다음과 같은 경우에는 `interface`가 자연스러울 수 있습니다.
객체의 공개 구조 정의
interface User {
id: number;
name: string;
}확장을 전제로 한 객체 구조
interface AdminUser
extends User {
permissions: string[];
}클래스가 구현할 계약
interface Serializable {
serialize(): string;
}외부에서 타입을 보강할 가능성이 있는 구조
interface Window {
appVersion: string;
}선언 병합이 필요한 라이브러리 타입
인터페이스의 개방적인 성격을 활용할 수 있습니다.
45. 개방형과 폐쇄형으로 이해하기
`interface`와 `type`의 성격을 다음처럼 기억할 수 있습니다.
interface
같은 이름으로 다시 열 수 있음
선언 병합 가능
개방형 설계도
type
한 번 이름을 정의하면 다시 선언 불가
새 타입으로 조합해야 함
폐쇄형 이름표예를 들어 라이브러리 사용자가 나중에 속성을 추가하도록 허용하려면 인터페이스의 개방성이 유용할 수 있습니다.
반대로 타입의 가능한 경우를 정확하게 닫아두고 싶다면 `type`이 자연스러울 수 있습니다.
개방형이 항상 좋고 폐쇄형이 항상 안전한 것은 아닙니다.
건물의 증축 가능 여부는 사용 목적에 따라 달라집니다.
46. 프로젝트에서는 하나의 기준을 정하자
실무에서는 팀마다 규칙이 다를 수 있습니다.
다음과 같은 규칙을 사용할 수 있습니다.
객체 구조
→ interface
유니언, 튜플, 함수 타입
→ type또는 다음처럼 정할 수도 있습니다.
기본적으로 type 사용
선언 병합이나 클래스 계약이 필요할 때
→ interface 사용반대로 객체 설계에는 인터페이스를 우선하고, 인터페이스로 표현하기 어려운 타입만 `type`으로 만들 수도 있습니다.
중요한 것은 절대적인 정답보다 일관성입니다.
같은 목적의 객체 타입이 어떤 파일에서는 `type`, 다른 파일에서는 `interface`로 무작위 작성되면 팀원이 매번 숨은 의도를 추리해야 합니다.
코드 리뷰가 타입 철학 토론회로 변하기 전에 기준을 정하는 편이 좋습니다.
47. 같은 프로젝트에서 함께 사용하기
두 문법은 경쟁자가 아니라 서로 보완하는 도구가 될 수 있습니다.
type UserRole =
| "admin"
| "manager"
| "user";객체 구조는 인터페이스로 작성합니다.
interface User {
readonly id: number;
name: string;
role: UserRole;
}사용자 상태는 타입 별칭으로 정의합니다.
type UserStatus =
| "active"
| "inactive"
| "blocked";인터페이스에서 사용합니다.
interface UserAccount
extends User {
status: UserStatus;
}실제 객체를 생성합니다.
const account: UserAccount = {
id: 1,
name: "김타입",
role: "admin",
status: "active",
};각 문법이 잘하는 일을 나눠 맡았습니다.
type
→ 가능한 값 조합
interface
→ 객체 구조와 확장 관계공구함에서 망치와 드라이버가 서로 우승자를 가릴 필요는 없습니다.
나사 앞에서는 드라이버가 일하고, 못 앞에서는 망치가 일하면 됩니다. 🔨
48. 실습 1: 사용자 권한 구조 만들기
역할 타입을 정의합니다.
type UserRole =
| "admin"
| "manager"
| "user";기본 사용자 인터페이스를 만듭니다.
interface User {
readonly id: number;
name: string;
email: string;
role: UserRole;
}프로필 인터페이스를 만듭니다.
interface UserProfile {
nickname?: string;
profileImage?: string;
introduction?: string;
}일반 사용자 계정을 확장합니다.
interface UserAccount
extends
User,
UserProfile {
isActive: boolean;
lastLoginAt?: string;
}관리자 계정을 다시 확장합니다.
interface AdminAccount
extends UserAccount {
permissions: string[];
}객체를 생성합니다.
const admin: AdminAccount = {
id: 1,
name: "김관리자",
email: "admin@example.com",
role: "admin",
nickname: "시스템관리자",
isActive: true,
permissions: [
"user:read",
"user:write",
"settings:write",
],
};관리자 정보를 출력합니다.
function printAdmin(
admin: AdminAccount
): void {
const displayName =
admin.nickname
?? admin.name;
console.log(
`관리자: ${displayName}`
);
console.log(
`권한 수: ${
admin.permissions.length
}`
);
}49. 실습 2: 결제 수단 설계하기
공통 결제 인터페이스를 정의합니다.
interface PaymentMethod {
readonly id: string;
name: string;
pay(amount: number): void;
}카드 결제 정보를 확장합니다.
interface CardPayment
extends PaymentMethod {
cardCompany: string;
lastFourDigits: string;
}계좌 결제 정보를 확장합니다.
interface BankPayment
extends PaymentMethod {
bankName: string;
accountNumber: string;
}카드 결제 객체를 만듭니다.
const card: CardPayment = {
id: "PAYMENT-001",
name: "개인 카드",
cardCompany: "타입카드",
lastFourDigits: "1234",
pay(amount) {
console.log(
`${amount.toLocaleString()}원을 ` +
`${this.cardCompany} 카드로 결제합니다.`
);
},
};결제를 실행합니다.
card.pay(120000);공통 인터페이스 덕분에 결제 수단이 달라도 같은 방식으로 사용할 수 있습니다.
function processPayment(
method: PaymentMethod,
amount: number
): void {
method.pay(amount);
}processPayment(
card,
120000
);50. 실습 3: 알림 시스템 설계하기
알림 종류는 리터럴 유니언 타입으로 만듭니다.
type NotificationLevel =
| "info"
| "success"
| "warning"
| "error";기본 알림 인터페이스입니다.
interface Notification {
readonly id: number;
level: NotificationLevel;
message: string;
createdAt: string;
}읽음 상태를 가진 알림을 확장합니다.
interface ReadableNotification
extends Notification {
isRead: boolean;
read(): void;
}객체를 생성합니다.
const notification:
ReadableNotification = {
id: 1,
level: "success",
message: "저장되었습니다.",
createdAt: "2026-08-02",
isRead: false,
read() {
this.isRead = true;
},
};알림을 읽음 처리합니다.
notification.read();
console.log(
notification.isRead
);출력 결과:
true51. 실습 4: 선언 병합 이해하기
애플리케이션 설정 인터페이스를 먼저 선언합니다.
interface AppConfig {
appName: string;
}다른 파일이나 기능 영역에서 설정을 추가했다고 가정해 보겠습니다.
interface AppConfig {
version: string;
}또 다른 기능이 설정을 추가합니다.
interface AppConfig {
debug: boolean;
}최종적으로 `AppConfig`는 세 속성을 모두 가집니다.
const config: AppConfig = {
appName: "TypeMaster",
version: "1.0.0",
debug: true,
};속성 하나라도 빠지면 오류가 발생합니다.
const config: AppConfig = {
appName: "TypeMaster",
};선언 병합은 확장성이 필요한 타입에서 유용하지만, 여러 위치에서 무심코 같은 이름을 사용하면 예상하지 못한 구조가 생길 수도 있습니다.
52. 자주 발생하는 실수
실수 1. 인터페이스로 유니언 타입을 직접 만들기
interface Status =
| "idle"
| "loading";이 문법은 사용할 수 없습니다.
유니언 타입은 `type`으로 정의합니다.
type Status =
| "idle"
| "loading";실수 2. `type`에 `extends` 사용하기
type AdminUser
extends User {
permissions: string[];
}`type` 별칭은 이런 문법으로 확장하지 않습니다.
교차 타입을 사용합니다.
type AdminUser =
User & {
permissions: string[];
};실수 3. 인터페이스 확장 시 부모 속성을 누락하기
interface User {
id: number;
name: string;
}interface Admin
extends User {
permissions: string[];
}const admin: Admin = {
permissions: [
"user:read",
],
};`id`와 `name`이 누락되어 오류가 발생합니다.
실수 4. 충돌하는 인터페이스를 동시에 확장하기
interface A {
id: string;
}
interface B {
id: number;
}
interface C
extends A, B {}`id` 타입이 충돌합니다.
공통 속성의 타입을 통일하거나 구조를 다시 설계해야 합니다.
실수 5. 선언 병합을 타입 재정의로 생각하기
interface User {
id: number;
}interface User {
name: string;
}두 번째 선언이 첫 번째를 덮어쓰는 것이 아닙니다.
두 내용이 합쳐집니다.
interface User {
id: number;
name: string;
}실수 6. 같은 이름의 `type`을 다시 선언하기
type User = {
id: number;
};
type User = {
name: string;
};타입 별칭은 선언 병합을 지원하지 않습니다.
새로운 타입을 만들거나 교차 타입을 사용합니다.
실수 7. 클래스가 인터페이스 메서드를 자동으로 받는다고 생각하기
interface Printable {
print(): void;
}class Report
implements Printable {
}인터페이스는 구현 코드를 제공하지 않습니다.
클래스에서 직접 메서드를 구현해야 합니다.
class Report
implements Printable {
print(): void {
console.log(
"보고서를 출력합니다."
);
}
}실수 8. 무조건 하나의 문법만 사용하려고 하기
객체 타입에는 인터페이스가 자연스러울 수 있고, 유니언이나 튜플에는 타입 별칭이 필요합니다.
type Role =
| "admin"
| "user";
interface User {
id: number;
role: Role;
}두 문법을 목적에 맞게 함께 사용할 수 있습니다.
53. `type`과 `interface` 선택 공식
어떤 문법을 사용할지 고민될 때 다음 질문을 확인해 보세요.
질문 1. 객체 구조를 정의하는가?
둘 다 사용할 수 있습니다.
type User = {
id: number;
};interface User {
id: number;
}프로젝트 규칙을 따릅니다.
질문 2. 유니언 타입인가?
`type`을 사용합니다.
type Status =
| "loading"
| "success";질문 3. 튜플 타입인가?
`type`이 자연스럽습니다.
type Coordinate = [
number,
number,
];질문 4. 객체 구조를 `extends`로 확장하고 싶은가?
`interface`가 읽기 좋을 수 있습니다.
interface Admin
extends User {
permissions: string[];
}질문 5. 여러 독립 타입을 조합하고 싶은가?
교차 타입을 고려할 수 있습니다.
type Admin =
User &
PermissionInfo;질문 6. 같은 이름의 타입을 나중에 확장해야 하는가?
선언 병합이 가능한 `interface`를 고려합니다.
질문 7. 외부 라이브러리 타입을 보강해야 하는가?
`interface`와 선언 병합이 유용할 수 있습니다.
질문 8. 가능한 타입을 닫힌 목록으로 관리하는가?
`type`의 리터럴 유니언이 적합합니다.
type Theme =
| "light"
| "dark";질문 9. 클래스가 구현할 계약인가?
`interface`가 의미를 잘 전달할 수 있습니다.
interface Serializable {
serialize(): string;
}질문 10. 팀 규칙이 이미 정해져 있는가?
특별한 이유가 없다면 팀 규칙을 따릅니다.
일관성은 작은 문법 취향보다 큰 가치를 가집니다.
54. 미니 퀴즈
문제 1
다음 인터페이스에서 선택적 속성은 무엇일까요?
interface User {
readonly id: number;
name: string;
nickname?: string;
}정답
`nickname`입니다.
`?`가 붙어 있으므로 생략할 수 있습니다.
문제 2
다음 코드에서 오류가 발생하는 이유는 무엇일까요?
interface User {
id: number;
name: string;
}
const user: User = {
id: 1,
};정답
필수 속성인 `name`이 빠졌기 때문입니다.
문제 3
다음 확장 결과에 필요한 속성을 모두 작성해 보세요.
interface User {
id: number;
name: string;
}
interface Admin
extends User {
permissions: string[];
}정답
const admin: Admin = {
id: 1,
name: "김관리자",
permissions: [
"user:read",
],
};문제 4
다음 두 인터페이스 선언은 어떻게 처리될까요?
interface Product {
id: number;
}interface Product {
name: string;
}정답
선언 병합이 발생합니다.
최종 `Product`에는 `id`와 `name`이 모두 필요합니다.
문제 5
같은 이름의 `type` 별칭을 두 번 선언할 수 있을까요?
type Product = {
id: number;
};
type Product = {
name: string;
};정답
불가능합니다.
중복된 타입 이름으로 오류가 발생합니다.
문제 6
다음 타입은 `interface`와 `type` 중 무엇이 적합할까요?
"small" | "medium" | "large"정답
`type`이 적합합니다.
type ButtonSize =
| "small"
| "medium"
| "large";문제 7
다음 타입은 두 문법 중 무엇을 사용할 수 있을까요?
id: number
name: string
email: string정답
객체 구조이므로 `type`과 `interface` 모두 사용할 수 있습니다.
문제 8
다음 코드에서 오류가 발생하는 이유는 무엇일까요?
interface StringItem {
value: string;
}
interface NumberItem {
value: number;
}
interface Item
extends
StringItem,
NumberItem {}정답
`value` 속성의 타입이 `string`과 `number`로 충돌하기 때문입니다.
문제 9
인터페이스가 메서드의 실제 코드를 제공할까요?
정답
아닙니다.
인터페이스는 메서드의 이름, 매개변수, 반환 타입만 정의합니다.
실제 구현은 객체나 클래스에서 작성해야 합니다.
문제 10
다음 상황에 적합한 문법을 연결해 보세요.
객체 확장과 선언 병합
유니언 타입
튜플 타입
클래스 구현 계약정답 예시
객체 확장과 선언 병합
→ interface
유니언 타입
→ type
튜플 타입
→ type
클래스 구현 계약
→ interface55. 핵심 정리
인터페이스 정의
interface User {
id: number;
name: string;
}객체가 가져야 할 구조를 정의합니다.
선택적 속성과 읽기 전용 속성
interface User {
readonly id: number;
name: string;
nickname?: string;
}메서드 정의
interface User {
name: string;
greet(): string;
}인터페이스 확장
interface AdminUser
extends User {
permissions: string[];
}다중 확장
interface Product
extends
Identifiable,
Timestamped {
name: string;
price: number;
}선언 병합
interface User {
id: number;
}
interface User {
name: string;
}최종적으로 두 속성이 합쳐집니다.
타입 별칭은 선언 병합 불가
type User = {
id: number;
};같은 이름으로 다시 선언할 수 없습니다.
`type`의 강점
type Status =
| "idle"
| "loading"
| "success";
type Coordinate = [
number,
number,
];유니언과 튜플 등 다양한 타입 표현에 사용할 수 있습니다.
`interface`의 강점
interface Admin
extends User {
permissions: string[];
}객체 구조 확장, 선언 병합, 클래스 계약에 자연스럽습니다.
두 문법은 함께 사용할 수 있음
type UserRole =
| "admin"
| "user";
interface User {
id: number;
role: UserRole;
}56. 마무리
`type`과 `interface`는 모두 TypeScript에서 객체의 구조를 정의할 수 있습니다.
type TypeUser = {
id: number;
name: string;
};interface InterfaceUser {
id: number;
name: string;
}선택적 속성, 읽기 전용 속성, 메서드 등 객체 타입의 기본 기능도 비슷하게 사용할 수 있습니다.
하지만 두 문법의 성격에는 차이가 있습니다.
`type`은 다양한 타입 표현에 이름을 붙일 수 있는 범용 도구입니다.
type Status =
| "idle"
| "loading"
| "success";
type Coordinate = [
number,
number,
];`interface`는 객체 구조를 확장하고 다시 열 수 있는 객체 중심의 설계 도구입니다.
interface AdminUser
extends User {
permissions: string[];
}interface User {
profileImage?: string;
}두 문법의 핵심 차이를 한 문장으로 정리하면 다음과 같습니다.
`type`은 타입 표현을 조합하는 데 유연하고, `interface`는 객체 구조를 확장하는 데 자연스럽다.
실무에서는 다음처럼 사용할 수 있습니다.
객체 구조와 클래스 계약
→ interface
유니언, 튜플, 복잡한 타입 조합
→ type또는 프로젝트 전체에서 객체 타입까지 `type`으로 통일할 수도 있습니다.
중요한 것은 문법의 승자를 정하는 것이 아닙니다.
데이터의 의도와 팀의 규칙이 코드에서 분명하게 보이도록 선택하는 것입니다.
`type`과 `interface`는 서로 왕좌를 두고 싸우는 라이벌이 아닙니다.
하나는 자유롭게 재료를 조합하는 공구함이고, 다른 하나는 확장 가능한 조립식 설계도입니다.
좋은 개발자는 한쪽 공구를 봉인하지 않습니다.
필요한 순간에 알맞은 도구를 꺼냅니다. 🧱
57. 다음 편 예고
지금까지 우리는 `type`과 `interface`를 사용해 객체의 속성 이름을 미리 정의했습니다.
interface User {
id: number;
name: string;
email: string;
}이 객체는 `id`, `name`, `email`처럼 정해진 속성을 가집니다.
그런데 객체의 속성 이름을 미리 알 수 없는 상황도 있습니다.
const scores = {
kim: 90,
lee: 85,
park: 100,
};학생이 추가될 때마다 속성 이름도 달라집니다.
scores.choi = 95;언어별 번역 메시지를 관리할 수도 있습니다.
const messages = {
ko: "안녕하세요.",
en: "Hello.",
ja: "こんにちは。",
};키 이름은 달라지지만 모든 값이 문자열이라는 규칙은 유지됩니다.
이런 동적인 객체의 타입은 어떻게 작성해야 할까요?
interface ScoreMap {
[studentName: string]: number;
}TypeScript에는 동적인 키와 값의 타입을 정의하는 인덱스 시그니처가 있습니다.
또한 키와 값 타입을 이용해 객체 구조를 만드는 `Record`도 있습니다.
type ScoreMap =
Record<string, number>;두 문법은 어떻게 다를까요?
특정 키만 허용하는 객체도 만들 수 있을까요?
type ThemeColors =
Record<
"primary" | "secondary",
string
>;다음 편에서는 객체의 속성 이름이 실행 중에 달라지는 상황을 안전하게 표현하는 방법을 살펴보겠습니다.
다음 이야기
[TypeScript 완전정복 #14] 객체의 속성 이름을 미리 모른다면? | 인덱스 시그니처와 Record 제대로 이해하기
- 동적인 객체 키는 어떻게 표현할까요?
- 인덱스 시그니처는 무엇일까요?
- 문자열 키와 숫자 키는 어떻게 다를까요?
- 모든 속성 값은 같은 타입이어야 할까요?
- 고정 속성과 인덱스 시그니처를 함께 사용할 수 있을까요?
- 잘못된 키로 객체에 접근하면 어떻게 될까요?
- `Record<Key, Value>`는 무엇일까요?
- 리터럴 유니언과 `Record`를 함께 사용하면 무엇이 좋을까요?
- 인덱스 시그니처와 `Record`는 언제 구분해서 사용할까요?
- 동적 객체보다 `Map`이 더 적합한 경우는 언제일까요?
다음 편에서는 이름이 계속 바뀌는 객체 속성들에게 질서 있는 명찰을 달아주는 방법을 알아보겠습니다.
