타입이 다른데 할당이 되는 이유 — TypeScript는 구조적 타이핑
문제 발생
서로 의미가 완전히 다른 두 인터페이스(UserInput과 UserDto)인데, 실수로 하나를 다른 하나가 필요한 곳에 그대로 넘겼는데도 컴파일 에러가 나지 않았습니다.
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)**을 씁니다 — 두 타입이 이름은 다르더라도, 실제로 가진 속성의 구조가 호환되면 서로 대입 가능하다고 판단합니다.
UserInput과 UserDto는 이름은 다르지만 둘 다 { name: string; email: string }이라는 같은 구조를 가지므로, TypeScript는 이 둘을 호환 가능하다고 봅니다. 이건 버그가 아니라 TypeScript의 핵심 설계 철학입니다 — JavaScript의 덕 타이핑(duck typing) 특성과 잘 맞도록 의도적으로 선택된 방식입니다.
해결 방안
- 대부분의 경우 구조적 타이핑은 유연성을 주는 장점이지만, 의미적으로 절대 섞이면 안 되는 타입(예:
UserId와PostId처럼 둘 다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); // 에러 — 구조는 같아도 브랜드가 달라 호환 안 됨- 단순 데이터 형태 타입(
UserInput,UserDto처럼 순수 데이터 구조)이라면 굳이 구조적 타이핑을 우회하려 하지 말고, 애초에 두 타입을 굳이 나눌 필요가 있는지부터 검토합니다 — 정말 다른 의미라면 함수/모듈 경계에서 명시적으로 변환하는 과정을 두는 것이 더 근본적인 해결책입니다. - 클래스는
private/protected멤버가 있으면 구조적 호환 검사에서 그 멤버의 "출처(어느 클래스 선언에서 왔는가)"까지 함께 비교하므로, 이름이 같은 필드라도 서로 다른 클래스의 private 필드는 호환되지 않습니다 — 완전히 순수한 구조적 타이핑은 아니라는 점도 참고할 만합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.