~9 min · types-interfaces, excess-property, object-literal, structural-typing
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"구조적 타이핑은 여분을 허용하지만, 방금 적은 낯선 속성에는 ‘오타 아니야?’라고 한 번 더 묻는다."
겉보기의 모순
TypeScript는 필요한 속성을 가진 값에 추가 속성이 있어도 대체로 허용해. 그런데 takeUser({ name: 'Pippa', emali: '...' })처럼 객체 리터럴을 바로 넘기면 알려지지 않은 emali를 오류로 잡아. 구조적 타이핑과 모순처럼 보이지만 목적이 다른 두 검사야.
기존 변수는 이미 다른 문맥에서 쓰이는 값일 수 있으니 필요한 부분만 맞으면 받아. 반면 지금 막 기대 타입에 맞춰 적은 리터럴에 낯선 속성이 있으면 오타일 확률이 높아. 그래서 새 리터럴에만 더 엄격한 ‘신선도’ 검사를 적용해.
검사를 피하는 법보다 뜻을 읽어
리터럴을 변수에 먼저 담거나 타입 단언을 쓰면 검사를 우회할 수 있어. 하지만 오류가 실제 오타라면 우회는 해결이 아니야. 속성 이름을 고치거나, 계약에 정말 필요한 새 속성이라면 타입을 확장해야 해.
동적 키를 받아야 하는 객체라면 인덱스 시그니처나 Record를 명시해. 어떤 문자열 키든 허용한다는 계약과, 정해진 속성만 있는 객체에 우연히 낯선 키가 들어간 상황을 구별하는 거야.
API 설계에 주는 신호
호출자가 자꾸 알 수 없는 속성을 넘기고 싶어 한다면 API의 입력 모양이 너무 닫혀 있거나 확장 지점이 잘못되었을 수 있어. 반대로 단언으로 초과 속성 검사를 계속 끄고 있다면 오타 방지 장치를 스스로 무력화하는 중일 수 있지.
대표 오류 메시지는 ‘객체 리터럴은 알려진 속성만 지정할 수 있다’는 뜻이야. { widht: 100 }처럼 속성 이름을 잘못 적은 경우를 잡는 것이 목적이고, 실제로는 열 번 가운데 아홉 번쯤 이런 오류가 단순 오타를 찾아 줘.
초과 속성 오류는 방해가 아니라 질문이야. 오타인지, 계약이 빠졌는지, 정말 열린 사전이 필요한지 답하고 그에 맞게 타입을 고쳐.
피파의 고백
초과 속성 검사는 처음엔 까다로워 보여도 대부분 막 적은 객체의 오타를 가장 가까운 자리에서 잡아 줘. 우회하기 전에 정말 새 속성이 필요한지부터 확인하는 편이 늘 쌌어.
Code
규칙이 발동할 때 — 그리고 안 할 때·typescript
interface Box {
width: number;
height: number;
}
function render(b: Box) {}
// Literal 직접 전달 — excess property 체크 발동.
render({ width: 100, height: 200, depth: 50 });
// ^ Object literal may only specify known properties
// 같은 값을 변수 통해 — structural 규칙, 체크 스킵.
const withExtras = { width: 100, height: 200, depth: 50 };
render(withExtras); // ✅ 받아들임
// Typo 잡기:
render({ width: 100, hieght: 200 });
// ^ Object literal may only specify known properties
// Did you mean to write 'height'?
// 이 typo 가 순수 structural 규칙에선 빠져나갔을 거.
// Excess property 체크가 literal 용 안전망.
세 가지 우회법 — 하나만 권장·typescript
// 정당하게 추가 원할 때 escape hatch 3개.
interface Config {
port: number;
}
function setup(c: Config) {}
// 1. Index signature — 추가가 설계상 허용.
interface OpenConfig {
port: number;
[key: string]: unknown;
}
function setupOpen(c: OpenConfig) {}
setupOpen({ port: 5173, host: 'localhost' }); // ✅
// 2. 먼저 변수 — 의도의 암묵 신호.
const c = { port: 5173, host: 'localhost' };
setup(c); // ✅ (추가 'host' 받아들임)
// 3. As-cast — 아껴 써, 주석과 함께만.
setup({ port: 5173, host: 'localhost' } as Config); // ✅ 근데 sloppy
// 더 나음: Config 에 'host' 추가하거나, Config 에 index signature 쓰게.
status: number와 data: unknown을 가진 ApiResponse를 정의해. handle({ status: 200, data: 'hi', cached: true })처럼 객체 리터럴을 바로 넘긴 경우와, 같은 객체를 먼저 변수에 담아 넘긴 경우를 비교해. 오류가 달라지는 이유를 초과 속성 검사와 구조적 호환성으로 설명해.
Hint
객체 리터럴을 바로 넘기면 초과 속성 검사가 오타를 잡아. 변수에 담은 뒤 넘길 때는 구조적 호환만 확인하므로 추가 필드가 허용될 수 있어.
Progress
Progress is local-only — sign in to sync across devices.