Chapter 04

인터페이스 (Interface) & 타입 별칭 (Type Alias)

복잡한 타입 (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에 없음
};

인터페이스 vs 인라인 객체 타입

언제 어떤 것을 사용할까? 재사용이 필요하면 인터페이스, 일회성이면 인라인 객체 타입을 사용합니다.
// 방법 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 인터페이스 하나로 여러 함수에서 활용!

선택적 속성 (Optional Property)

// ? 를 붙이면 선택적 속성
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)

// 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 };
}

인덱스 시그니처 (Index Signature)

동적 속성 이름을 가진 객체 타입을 정의합니다.
속성 이름을 미리 알 수 없을 때, "어떤 이름이든 이 타입의 값을 가진다"고 정의하는 방법입니다.

// 문자열 키에 대한 인덱스 시그니처: [키이름: 키타입]: 값타입
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 타입
};

인터페이스 확장 (extends)

// 기본 인터페이스 (부모 역할)
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;
}

다단계 상속 패턴 실전 예시

실전 패턴: BaseEntity에서 시작하여 도메인별로 단계적으로 확장하면, 공통 속성 중복 없이 체계적인 타입 계층을 구성할 수 있습니다.
// 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 Alias)

타입 별칭이란 복잡한 타입에 짧은 이름을 붙여주는 것입니다. 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 vs Type 비교

기능 interface type
객체 구조 정의 O O
extends (확장) O (extends) O (& 교차)
implements (구현) O O
선언 병합 O (자동 병합) X
유니온/교차 타입 X O
원시 타입 별칭 X O
튜플/매핑 타입 X O
일반 규칙: 객체 구조 정의 → interface, 유니온/교차/원시 → type. 팀 컨벤션을 따르세요.

선언 병합 (Declaration Merging)

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 };
}

교차 타입 (Intersection Type)

&로 여러 타입을 합쳐 모든 속성을 동시에 가진 타입을 생성합니다.
유니온(|)이 "이것 또는 저것"이라면, 인터섹션(&)은 "이것 그리고 저것 모두"입니다.

// 기본 교차 타입
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;
};

판별 유니온 (Discriminated Union)

핵심 패턴

공통 판별 속성(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;
  }
}

유틸리티 타입 (Utility Type): Partial, Required

유틸리티 타입이란 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}`);
}

유틸리티 타입: Pick, Omit

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, Readonly

// 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"; }

Chapter 4 정리

개념 문법 용도
인터페이스interface User { }객체 구조 정의
선택적 속성name?: string생략 가능한 속성
읽기 전용readonly id: string불변 속성
인덱스 시그니처[key: string]: T동적 키
extendsinterface B extends A확장
타입 별칭type Point = { }유니온, 교차 등
교차 타입A & B타입 합치기
판별 유니온{ type: "x" } | { type: "y" }안전한 분기
Partial/RequiredPartial<T>선택적/필수 변환
Pick/OmitPick<T, K>속성 선택/제외
RecordRecord<K, V>키-값 타입 객체
Next

수고하셨습니다!


다음 챕터: 클래스 →


← 목차로 돌아가기