본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev20

타입이 다른데 할당이 되는 이유 — TypeScript는 구조적 타이핑

  • #Engineering Note
  • #Type Safety
  • #TypeScript

문제 발생

서로 의미가 완전히 다른 두 인터페이스(UserInputUserDto)인데, 실수로 하나를 다른 하나가 필요한 곳에 그대로 넘겼는데도 컴파일 에러가 나지 않았습니다.

interface UserInput {
  name: string;
  email: string;
}

interface UserDto {
  name: string;
  email: string;
}

function saveUser(dto: UserDto) { /* ... */ }

const input: UserInput = { name: "a", email: "[email protected]" };
saveUser(input); // 에러 없이 통과됨

원인 분석

Java나 C# 같은 명목적 타이핑(nominal typing) 언어는 타입이 명시적으로 선언(상속, 구현)된 관계여야만 호환됩니다. 반면 TypeScript는 **구조적 타이핑(structural typing)**을 씁니다 — 두 타입이 이름은 다르더라도, 실제로 가진 속성의 구조가 호환되면 서로 대입 가능하다고 판단합니다.

UserInputUserDto는 이름은 다르지만 둘 다 { name: string; email: string }이라는 같은 구조를 가지므로, TypeScript는 이 둘을 호환 가능하다고 봅니다. 이건 버그가 아니라 TypeScript의 핵심 설계 철학입니다 — JavaScript의 덕 타이핑(duck typing) 특성과 잘 맞도록 의도적으로 선택된 방식입니다.

해결 방안

  1. 대부분의 경우 구조적 타이핑은 유연성을 주는 장점이지만, 의미적으로 절대 섞이면 안 되는 타입(예: UserIdPostId처럼 둘 다 string이지만 바꿔 쓰면 안 되는 경우)이라면 "명목적 타이핑을 흉내 내는" 패턴을 씁니다 — 브랜드 타입(branded type).
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };

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

const postId = "123" as PostId;
getUser(postId); // 에러 — 구조는 같아도 브랜드가 달라 호환 안 됨
  1. 단순 데이터 형태 타입(UserInput, UserDto처럼 순수 데이터 구조)이라면 굳이 구조적 타이핑을 우회하려 하지 말고, 애초에 두 타입을 굳이 나눌 필요가 있는지부터 검토합니다 — 정말 다른 의미라면 함수/모듈 경계에서 명시적으로 변환하는 과정을 두는 것이 더 근본적인 해결책입니다.
  2. 클래스는 private/protected 멤버가 있으면 구조적 호환 검사에서 그 멤버의 "출처(어느 클래스 선언에서 왔는가)"까지 함께 비교하므로, 이름이 같은 필드라도 서로 다른 클래스의 private 필드는 호환되지 않습니다 — 완전히 순수한 구조적 타이핑은 아니라는 점도 참고할 만합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.