본문으로 건너뛰기

구조적 타이핑

핵심 내용

TypeScript의 타입은 크게 원시 타입(string, number, boolean 등)과 객체 타입으로 나뉩니다. 여기까진 자바스크립트를 알면 새로울 게 없습니다. 이 타입들이 서로 어떻게 호환되는지 판단하는 방식부터 얘기가 달라집니다.

Java처럼 이름과 선언 관계를 기준으로 타입을 판단하는 언어에 익숙하면 "이름이 다른데 왜 되지" 싶을 수 있습니다. TypeScript는 이름이 아니라 구조로 판단합니다. 어떤 타입이 요구하는 프로퍼티를 최소한 가지고 있으면 그걸로 충분합니다. 이걸 구조적 타이핑이라고 부릅니다.

구조가 우연히 같으면 이름이 완전히 다른 타입끼리도 호환됩니다. 이런 실수를 막으려면 브랜드 타입 같은 우회 기법이 따로 필요합니다.

예시

interface User {
name: string;
}

interface Admin {
name: string;
permission: string;
}

function greet(user: User) {
console.log(user.name);
}

const admin: Admin = { name: "kim", permission: "read" };
greet(admin); // OK, Admin이 User의 요구사항을 만족하니까

function greetAdmin(admin: Admin) {
console.log(admin.name);
}

const user: User = { name: "kim" };
greetAdmin(user); // 에러: permission이 없음
type UserId = string;
type ProductId = string;

function getUser(id: UserId) {
/* ... */
}

const pid: ProductId = "p-001";
getUser(pid); // 에러 없이 통과. 둘 다 결국 string이라 구분이 안 됨

헷갈리기 쉬운 점

구조적 타이핑의 예외처럼 보이는 상황이 하나 있습니다.

const obj = { name: "kim", age: 20 };
const u1: User = obj; // OK

const u2: User = { name: "kim", age: 20 }; // 에러: age는 없는 프로퍼티

같은 모양인데 위는 통과하고 아래는 막힙니다. TypeScript는 객체 리터럴을 직접 넣을 때만 오타 가능성이 높다고 보고 초과 프로퍼티 검사(excess property check)를 추가로 돕니다. 이미 변수에 담겨 있는 객체(obj)는 구조 호환성만 봅니다. 오타 방지용 안전장치라고 생각하면 납득이 됩니다.

실무에서 볼 점

  • 서로 다른 의미의 ID가 둘 다 string이면 TypeScript가 구분하지 못합니다. 필요하면 브랜드 타입 같은 방식으로 의미를 분리합니다.
  • API 응답 타입과 화면에서 쓰는 타입이 우연히 호환될 수 있으니, 이름보다 실제 프로퍼티 구조를 먼저 봅니다.

예상 질문

구조적 타이핑에서 타입 호환성은 무엇을 기준으로 판단하나요?

타입 이름보다 객체가 가진 프로퍼티 구조를 기준으로 판단합니다.

요구하는 프로퍼티를 최소한 가지고 있으면 호환되고, 부족하면 호환되지 않습니다. 단, 객체 리터럴을 직접 대입할 때는 오타를 막기 위해 초과 프로퍼티 검사가 추가로 걸립니다.