| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- Visual Studio Code
- 테스트자동화
- junit
- 스프링부트
- 단위테스트
- Java
- SpringBoot
- list
- IntelliJ
- ArrayList
- math
- 배열
- 인텔리제이
- input
- 정규식
- Eclipse
- HashMap
- string
- 자바문법
- java테스트
- 자바스크립트
- 문자열
- javascript
- vscode
- js
- CSS
- Array
- junit5
- 자바
- html
- Today
- Total
어제 오늘 내일
[TypeScript] 9. 구조적 타이핑 — TypeScript가 타입을 비교하는 방식 본문
이 글의 목표: "왜 이 타입이 저 타입에 할당되지?" 같은 혼란을 없앱니다. TypeScript 타입 호환성의 근본 원리를 이해합니다.
이름이 아니라 "모양"으로 판단한다
Java나 C#은 타입 이름이 같아야 호환됩니다(명목적 타이핑, nominal typing). TypeScript는 다릅니다. 모양(구조)이 같으면 호환됩니다. 이것이 구조적 타이핑(structural typing)입니다.
interface Point {
x: number;
y: number;
}
// Point라고 선언하지 않았지만, x와 y를 가진 객체
const coord = { x: 10, y: 20 };
// 모양이 Point와 같으므로 할당된다 — 이름은 상관없다
const p: Point = coord; // ✅
이걸 "덕 타이핑(duck typing)"이라고도 부릅니다. "오리처럼 걷고 오리처럼 울면 오리다."
더 많은 속성은 괜찮다
필요한 속성을 모두 가지고 있으면, 추가 속성이 있어도 할당됩니다.
interface Named {
name: string;
}
const person = { name: "철수", age: 30 };
// person은 name을 가지므로 Named로 취급 가능 (age가 남아도 OK)
const n: Named = person; // ✅
이 원리는 함수에도 적용됩니다.
function printName(obj: { name: string }) {
console.log(obj.name);
}
// name을 포함하면 다른 속성이 있어도 넘길 수 있다
printName({ name: "영희", age: 25 }); // ✅
예외: 객체 리터럴의 "초과 속성 검사"
방금 규칙에 예외가 하나 있습니다. 객체 리터럴을 직접 할당할 때는, 정의에 없는 속성이 있으면 오히려 에러가 납니다. 오타를 잡기 위한 안전장치입니다.
interface Config {
url: string;
}
// 변수를 거치면 OK (구조적 타이핑)
const raw = { url: "/api", timeout: 5000 };
const c1: Config = raw; // ✅
// 리터럴을 직접 주면 초과 속성 검사가 작동한다
const c2: Config = { url: "/api", timeout: 5000 };
// ❌ 'timeout'은 Config에 없습니다. 오타입니까?
왜 이렇게 다를까요? 리터럴을 바로 쓸 때 나타나는 초과 속성은 "실수(오타)"일 가능성이 높기 때문입니다. TypeScript는 이 흔한 실수를 잡아줍니다.
interface Options {
color?: string;
}
// color를 colour로 오타 → 초과 속성 검사가 즉시 잡아준다
const opt: Options = { colour: "red" };
// ❌ 'colour'는 Options에 없습니다. 'color'를 쓰려던 건가요?
함수 타입의 호환성
함수도 구조적으로 비교됩니다. 여기엔 살짝 직관에 반하는 규칙이 있습니다.
매개변수가 더 적은 함수는 호환된다
type Callback = (value: number, index: number) => void;
// index를 안 받는 함수도 Callback으로 쓸 수 있다
const cb: Callback = (value) => {
console.log(value); // index를 무시해도 문제없다
};
Array.forEach에 콜백을 넘길 때 인덱스를 생략할 수 있는 이유가 바로 이것입니다. 더 적은 인자를 받는 함수는 항상 안전하게 호출 가능합니다.
반환 타입: 더 구체적인 것은 호환된다
type GetValue = () => { name: string };
// 더 많은 속성을 반환해도 OK (name을 포함하므로)
const fn: GetValue = () => ({ name: "철수", age: 30 }); // ✅
브랜드 타입: 구조적 타이핑을 우회하기
가끔은 구조가 같아도 구분하고 싶을 때가 있습니다. 예를 들어 UserId와 PostId는 둘 다 string이지만 섞이면 안 됩니다.
// 브랜드(가상의 표식)를 붙여 명목적 타이핑을 흉내낸다
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };
// 브랜드를 부여하는 함수
function toUserId(id: string): UserId {
return id as UserId;
}
const uid = toUserId("u1");
function getUser(id: UserId) { /* ... */ }
getUser(uid); // ✅
// getUser("u1"); // ❌ 그냥 string은 UserId가 아니다
// getUser(postId); // ❌ PostId를 넣으면 막힌다
이 기법은 ID 뒤바뀜 같은 미묘한 버그를 컴파일 타임에 막아줍니다.
정리
- TypeScript는 이름이 아니라 모양으로 타입 호환을 판단한다(구조적 타이핑).
- 필요한 속성을 다 가지면, 추가 속성이 있어도 호환된다.
- 단, 객체 리터럴을 직접 할당할 때는 초과 속성 검사가 작동해 오타를 잡는다.
- 함수는 "더 적은 매개변수", "더 구체적인 반환값"이면 호환된다.
- 구조가 같아도 구분하고 싶다면 브랜드 타입을 쓴다.
여기까지가 Part 2입니다. 다음 글부터는 TypeScript의 진짜 강력한 무기, 제네릭으로 들어갑니다.
'IT > TypeScript' 카테고리의 다른 글
| [TypeScript] 10. 제네릭 기초 — 재사용 가능한 타입 만들기 (1) | 2026.08.10 |
|---|---|
| [TypeScript] 8. 함수와 타입 — 오버로드, 선택적 매개변수, this (0) | 2026.08.10 |
| [TypeScript] 7. 타입 좁히기(Narrowing) (0) | 2026.08.10 |
| [TypeScript] 6. 유니온과 인터섹션, 그리고 리터럴 타입 (0) | 2026.08.10 |
| [TypeScript] 5. interface vs type — 무엇을 언제 쓸까 (0) | 2026.08.09 |