복잡한 타입 (Type) 구조를 우아하게 정의하는 방법
인터페이스는 객체의 구조(shape)를 정의합니다.
어떤 프로퍼티 (Property, 속성)가 어떤 타입을 가져야 하는지 계약(contract)을 만듭니다.
인터페이스는 계약서와 같습니다 - "이 객체는 반드시 이런 형태를 가져야 해"라고 정의하는 것입니다.
// 인터페이스 정의: "User는 이런 형태여야 한다"
interface User {
name: string; // 이름은 문자열
age: number; // 나이는 숫자
email: string; // 이메일은 문자열
}
// 인터페이스에 맞는 객체 생성 (모든 속성을 빠짐없이 포함해야 함)
const user: User = {
name: "홍길동",
age: 25,
email: "hong@example.com"
};
// 속성 누락 -> 에러
const bad: User = {
name: "김철수"
// Error! age, email이 빠짐
};
// 없는 속성 추가 -> 에러
const also_bad: User = {
name: "이영희",
age: 30,
email: "lee@example.com",
phone: "010-1234" // Error! phone은 User에 없음
};
// 방법 1: 인터페이스로 이름 붙여 정의 (재사용 가능)
interface User {
name: string;
age: number;
}
function greetWithInterface(user: User): string {
return `안녕하세요, ${user.name}님!`;
}
// 방법 2: 인라인 객체 타입 (즉석 정의, 일회성)
function greetInline(user: { name: string; age: number }): string {
return `안녕하세요, ${user.name}님!`;
}
// 두 함수 모두 같은 객체를 받을 수 있음
const person = { name: "홍길동", age: 25 };
greetWithInterface(person); // OK
greetInline(person); // OK
// 인터페이스의 장점: 여러 곳에서 재사용
function isAdult(user: User): boolean {
return user.age >= 18;
}
function formatUser(user: User): string {
return `${user.name} (${user.age}세)`;
}
// User 인터페이스 하나로 여러 함수에서 활용!
// ? 를 붙이면 선택적 속성
interface User {
name: string;
age: number;
email?: string; // 선택적
nickname?: string; // 선택적
}
// email과 nickname 생략 가능
const user1: User = { name: "Kim", age: 25 };
const user2: User = { name: "Lee", age: 30, email: "lee@test.com" };
// 선택적 속성 접근 시 undefined 가능성 처리
function sendEmail(user: User): void {
if (user.email) {
// 이 블록에서 user.email은 string
console.log(`메일 발송: ${user.email}`);
} else {
console.log("이메일이 없습니다.");
}
// 옵셔널 체이닝
const domain = user.email?.split("@")[1]; // string | undefined
}
// readonly로 변경 불가 속성 만들기
interface Config {
readonly host: string;
readonly port: number;
readonly database: string;
}
const config: Config = {
host: "localhost",
port: 5432,
database: "mydb"
};
config.port = 3000; // Error! readonly 속성은 수정 불가
// 객체 생성 시에만 값 할당 가능
const newConfig: Config = {
host: "production.server.com",
port: 5432,
database: "prod_db"
};
// 실전 활용: 불변 데이터 모델
interface Point {
readonly x: number;
readonly y: number;
}
function movePoint(point: Point, dx: number, dy: number): Point {
// 원본 수정 대신 새 객체 반환
return { x: point.x + dx, y: point.y + dy };
}
동적 속성 이름을 가진 객체 타입을 정의합니다.
속성 이름을 미리 알 수 없을 때, "어떤 이름이든 이 타입의 값을 가진다"고 정의하는 방법입니다.
// 문자열 키에 대한 인덱스 시그니처: [키이름: 키타입]: 값타입
interface Dictionary {
[key: string]: string; // 어떤 문자열 키든, 값은 문자열
}
const translations: Dictionary = {
hello: "안녕하세요",
goodbye: "안녕히 가세요",
thanks: "감사합니다"
};
translations["welcome"] = "환영합니다"; // OK
// 숫자 키 인덱스 시그니처
interface NumberList {
[index: number]: string;
}
const fruits: NumberList = ["사과", "바나나", "체리"];
// 고정 속성 + 인덱스 시그니처 혼합
interface ApiResponse {
status: number; // 고정 속성
message: string; // 고정 속성
[key: string]: unknown; // 나머지 동적 속성
}
const response: ApiResponse = {
status: 200,
message: "OK",
data: { users: [] }, // OK - unknown 타입
timestamp: 1700000000 // OK - unknown 타입
};
// 기본 인터페이스 (부모 역할)
interface Animal {
name: string; // 동물 이름
age: number; // 동물 나이
}
// 확장: Animal의 속성을 물려받고 + 새 속성 추가
interface Dog extends Animal {
breed: string; // 견종 (Dog만의 속성)
bark(): void; // 짖기 메서드 (Method, 객체에 속한 함수)
}
interface Cat extends Animal {
indoor: boolean;
meow(): void;
}
const myDog: Dog = {
name: "멍멍이",
age: 3,
breed: "골든리트리버",
bark() { console.log("왈왈!"); }
};
// 다중 확장
interface ServiceDog extends Dog {
certificationId: string;
task: "guide" | "therapy" | "search";
}
// 여러 인터페이스 동시 확장
interface Pet extends Animal, Dog {
owner: string;
}
// 1단계: 모든 엔티티의 공통 속성
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
// 2단계: 사용자 기본 정보 (BaseEntity 확장)
interface User extends BaseEntity {
email: string;
name: string;
role: "user" | "admin";
}
// 3단계: 관리자 전용 정보 (User 확장)
interface AdminUser extends User {
role: "admin"; // 리터럴 타입으로 고정
permissions: string[]; // 권한 목록
department: string; // 소속 부서
}
// AdminUser는 BaseEntity + User + AdminUser의 모든 속성을 가짐
const admin: AdminUser = {
id: "admin-001",
createdAt: new Date(),
updatedAt: new Date(),
email: "admin@company.com",
name: "관리자",
role: "admin",
permissions: ["read", "write", "delete", "manage"],
department: "IT팀"
};
// 함수 타입 인터페이스
interface SearchFunction {
(source: string, query: string): boolean;
}
const search: SearchFunction = (source, query) => {
return source.includes(query);
};
// 호출 시그니처 + 속성 (하이브리드 타입)
interface Counter {
(start: number): void; // 호출 가능
count: number; // 속성도 가짐
reset(): void; // 메서드도 가짐
}
// 클래스에서 인터페이스 구현 (implements: "이 계약을 지키겠다")
interface Printable {
print(): string; // 출력 기능을 반드시 구현해야 함
}
interface Loggable {
log(level: string): void; // 로그 기능을 반드시 구현해야 함
}
// 두 인터페이스를 모두 구현하는 클래스
class Document implements Printable, Loggable {
constructor(private content: string) {} // 인스턴스 (Instance) 생성 시 내용 전달
print(): string {
return this.content;
}
log(level: string): void {
console.log(`[${level}] ${this.content}`);
}
}
타입 별칭이란 복잡한 타입에 짧은 이름을 붙여주는 것입니다. type 키워드를 사용합니다.
// 객체 타입 별칭: type 이름 = { ... }
type Point = {
x: number;
y: number;
};
// 원시 타입 별칭 (간단한 타입에 의미 있는 이름 부여)
type ID = string | number; // ID는 문자열이거나 숫자
type Name = string; // Name은 문자열
// 튜플 (Tuple) 타입 별칭
type Coordinate = [number, number]; // 좌표는 숫자 2개의 쌍
// 함수 타입 별칭
type Formatter = (value: number) => string; // 숫자를 문자열로 변환하는 함수
// 유니온 (Union) 타입 별칭
type Status = "active" | "inactive" | "pending"; // 3가지 중 하나
type Theme = "light" | "dark";
// 복합 타입 별칭
type User = {
id: ID;
name: Name;
status: Status;
location: Coordinate;
format: Formatter;
};
const user: User = {
id: "user-001",
name: "Kim",
status: "active",
location: [37.5665, 126.978],
format: (v) => v.toFixed(2)
};
| 기능 | interface | type |
|---|---|---|
| 객체 구조 정의 | O | O |
| extends (확장) | O (extends) | O (& 교차) |
| implements (구현) | O | O |
| 선언 병합 | O (자동 병합) | X |
| 유니온/교차 타입 | X | O |
| 원시 타입 별칭 | X | O |
| 튜플/매핑 타입 | X | O |
interface,
유니온/교차/원시 → type. 팀 컨벤션을 따르세요.
interface만의 고유 기능: 같은 이름으로 여러 번 선언하면 자동으로 합쳐집니다.
마치 문서에 내용을 추가하듯, 기존 인터페이스에 새 속성을 덧붙일 수 있습니다.
// 선언 1
interface Window {
title: string;
}
// 선언 2 - 자동으로 병합됨!
interface Window {
appVersion: string;
}
// 결과: Window = { title: string; appVersion: string; }
// 실전 활용: 라이브러리 타입 확장
// express의 Request 객체에 사용자 정보 추가
declare global {
namespace Express {
interface Request {
user?: {
id: string;
role: string;
};
}
}
}
// 이제 req.user를 타입 안전하게 사용 가능
// app.get("/", (req) => { req.user?.id; });
type은 같은 이름으로 재선언하면 에러가 발생합니다.
type은 병합이 불가능합니다.
// 문자열 리터럴 (Literal) 유니온: 허용되는 값을 나열
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// 타입 유니온: 서로 다른 구조의 객체
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
// 다양한 결과 타입
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function divide(a: number, b: number): Result<number> {
if (b === 0) return { success: false, error: "0으로 나눌 수 없습니다" };
return { success: true, data: a / b };
}
&로 여러 타입을 합쳐 모든 속성을 동시에 가진 타입을 생성합니다.
유니온(|)이 "이것 또는 저것"이라면, 인터섹션(&)은 "이것 그리고 저것 모두"입니다.
// 기본 교차 타입
type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };
type Person = HasName & HasAge & HasEmail;
const person: Person = {
name: "Kim",
age: 25,
email: "kim@example.com"
// 3가지 타입의 모든 속성이 필요!
};
// interface extends와 비교
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface SoftDeletable {
deletedAt: Date | null;
isDeleted: boolean;
}
// 교차 타입으로 조합
type BaseEntity = Timestamped & SoftDeletable;
type User = BaseEntity & {
id: string;
name: string;
email: string;
};
공통 판별 속성(discriminant, 구분자)으로 유니온 멤버를 구분하는 패턴입니다.
택배 상자에 "종류" 라벨을 붙여서, 라벨만 보고 내용물의 타입을 알 수 있는 것과 같습니다.
// 판별 속성: type (각 멤버마다 다른 리터럴 값을 가짐 -> 이걸로 구분)
type Action =
| { type: "LOGIN"; payload: { username: string; password: string } }
| { type: "LOGOUT" }
| { type: "FETCH_DATA"; payload: { url: string } }
| { type: "SET_ERROR"; payload: { message: string } };
// state: any는 예시를 간단히 하기 위한 것 (실제로는 구체적 타입 권장)
function reducer(state: any, action: Action) {
switch (action.type) {
case "LOGIN":
// action.payload.username 접근 가능
return { ...state, user: action.payload.username };
case "LOGOUT":
// payload 없음
return { ...state, user: null };
case "FETCH_DATA":
// action.payload.url 접근 가능
return { ...state, loading: true };
case "SET_ERROR":
// action.payload.message 접근 가능
return { ...state, error: action.payload.message };
}
}
// API 응답 타입 모델링
type ApiResponse<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string; retryAfter?: number };
function renderResponse<T>(response: ApiResponse<T>): string {
switch (response.status) {
case "loading":
return "로딩 중...";
case "success":
return `데이터: ${JSON.stringify(response.data)}`;
case "error":
return `에러: ${response.error}`;
}
}
// 결제 수단 모델링
type PaymentMethod =
| { type: "card"; cardNumber: string; expiryDate: string }
| { type: "bank"; bankName: string; accountNumber: string }
| { type: "mobile"; phoneNumber: string; provider: string };
function processPayment(method: PaymentMethod): void {
switch (method.type) {
case "card":
console.log(`카드 결제: ${method.cardNumber}`);
break;
case "bank":
console.log(`계좌 이체: ${method.bankName} ${method.accountNumber}`);
break;
case "mobile":
console.log(`모바일: ${method.provider} ${method.phoneNumber}`);
break;
}
}
유틸리티 타입이란 TypeScript가 기본 제공하는 "타입 변환 도구"입니다. 기존 타입을 변형해서 새 타입을 만듭니다.
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Partial<T> - 모든 속성을 선택적(?)으로 바꿈
type PartialUser = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number; }
// 업데이트 함수에서 유용: 바꾸고 싶은 속성만 전달하면 됨
function updateUser(id: string, updates: Partial<User>): User {
const user = getUserById(id); // 기존 사용자 조회
return { ...user, ...updates }; // 기존 값에 변경사항 덮어쓰기
}
updateUser("1", { name: "새 이름" }); // name만 변경 (나머지는 그대로)
updateUser("1", { email: "new@email.com" }); // email만 변경
// Required<T> - 모든 속성을 필수로 (선택적 ? 제거)
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
type RequiredConfig = Required<Config>;
// { host: string; port: number; debug: boolean; }
// 모든 설정이 확정된 최종 config
function startServer(config: Required<Config>): void {
console.log(`${config.host}:${config.port}`);
}
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// Pick<T, K> - 특정 속성만 골라내기 ("이것만 가져올래")
type UserProfile = Pick<User, "id" | "name" | "email">;
// { id: string; name: string; email: string; } -- 3개만 선택
// Omit<T, K> - 특정 속성만 제외하기 ("이것만 빼줘")
type UserWithoutPassword = Omit<User, "password">;
// { id: string; name: string; email: string; createdAt: Date; updatedAt: Date; }
// 실전 예: API 요청/응답 타입 분리
type CreateUserRequest = Pick<User, "name" | "email" | "password">;
type UserResponse = Omit<User, "password">;
type UpdateUserRequest = Partial<Pick<User, "name" | "email">>;
// 조합 예시
function createUser(data: CreateUserRequest): UserResponse {
const user: User = {
...data,
id: generateId(),
createdAt: new Date(),
updatedAt: new Date()
};
// password를 제외하고 반환
const { password, ...response } = user;
return response;
}
// Record<K, V> - 키(K) 타입과 값(V) 타입으로 객체 타입 생성 (사전처럼)
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete", "manage"],
editor: ["read", "write"],
viewer: ["read"]
};
// 점수판
type StudentScores = Record<string, number>;
const scores: StudentScores = {
"홍길동": 95,
"김철수": 88,
"이영희": 92
};
// Readonly<T> - 모든 속성을 readonly로
interface Config {
host: string;
port: number;
}
const config: Readonly<Config> = {
host: "localhost",
port: 3000
};
config.port = 8080; // Error! 모든 속성이 readonly
// 깊은 불변성을 위한 재귀 타입
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 패턴: Entity 시스템
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface User extends BaseEntity {
name: string;
email: string;
role: "admin" | "user";
}
interface Post extends BaseEntity {
title: string;
content: string;
authorId: string;
tags: string[];
status: "draft" | "published" | "archived";
}
// Create 요청: id, 날짜는 서버에서 자동 생성하므로 제외
// DTO (Data Transfer Object): 데이터를 주고받을 때 사용하는 타입
type CreateDTO<T extends BaseEntity> = Omit<T, keyof BaseEntity>;
type UpdateDTO<T extends BaseEntity> = Partial<CreateDTO<T>>;
// 사용
type CreateUserDTO = CreateDTO<User>;
// { name: string; email: string; role: "admin" | "user"; }
type UpdatePostDTO = UpdateDTO<Post>;
// { title?: string; content?: string; authorId?: string;
// tags?: string[]; status?: "draft" | "published" | "archived"; }
| 개념 | 문법 | 용도 |
|---|---|---|
| 인터페이스 | interface User { } | 객체 구조 정의 |
| 선택적 속성 | name?: string | 생략 가능한 속성 |
| 읽기 전용 | readonly id: string | 불변 속성 |
| 인덱스 시그니처 | [key: string]: T | 동적 키 |
| extends | interface B extends A | 확장 |
| 타입 별칭 | type Point = { } | 유니온, 교차 등 |
| 교차 타입 | A & B | 타입 합치기 |
| 판별 유니온 | { type: "x" } | { type: "y" } | 안전한 분기 |
| Partial/Required | Partial<T> | 선택적/필수 변환 |
| Pick/Omit | Pick<T, K> | 속성 선택/제외 |
| Record | Record<K, V> | 키-값 타입 객체 |