TypeScript 빈 객체 타입, {}로 충분할까?
배경
도면 삭제 API를 붙이면서 응답 data가 당분간 빈 객체({}) 형태로 올 가능성이 높았다.
이때 타입을 아래 셋 중 무엇으로 둘지 고민했다.
object{}Record<string, never>
겉보기에는 비슷하지만, 실제 허용 범위는 꽤 다르다.
내가 헷갈렸던 포인트
처음에는 object와 {}가 "빈 객체 타입"이라고 생각했다.
그래서 "셋 다 비슷한데 아무거나 써도 되지 않나?"라는 결론으로 가기 쉬웠다.
하지만 TypeScript에서 이 셋은 설계 의도 자체가 다르다.
object vs vs Record<string, never>
1) object: 원시값만 제외
object는 string/number/boolean 같은 원시값만 막고, 배열/함수/일반 객체를 모두 허용한다.
let a: object;
a = { ok: true }; // OK
a = []; // OK
a = () => {}; // OK
// a = 1; // Error (원시값 불가)
즉, "객체 계열 아무거나"에 가까움.
2) {}: null/undefined만 제외
{}는 생각보다 더 넓다. null, undefined만 아니면 대부분 들어온다.
let b: {};
b = { ok: true }; // OK
b = []; // OK
b = () => {}; // OK
b = 1; // OK
b = "text"; // OK
// b = null; // Error
// b = undefined; // Error
빈 객체 의미로 쓰기에는 너무 느슨하다.
3) Record<string, never>: 프로퍼티를 허용하지 않음
문자열 키 프로퍼티가 존재하면 안 되는 타입이다.
let c: Record<string, never>;
c = {}; // OK
// c = { id: 1 }; // Error (id: never에 number 할당 불가)
삭제 응답처럼 "지금 계약상 data는 비어 있어야 한다"를 가장 직접적으로 표현한다.
왜 실무에서 중요할까
타입은 코드 보조도구가 아니라 API 계약 문서다.
object,{}처럼 넓은 타입을 쓰면 계약 위반을 컴파일 단계에서 놓치기 쉽다.Record<string, never>처럼 의도를 좁히면 백엔드 응답 변경을 더 빨리 감지할 수 있다.- 리뷰에서 "왜 이 타입을 썼는지" 설명이 쉬워진다.
즉, 타입 선택은 취향이 아니라 변경 감지 전략에 가깝다.
트레이드오프와 선택 기준
장점은 명확하다.
- 의도 전달이 정확하다.
- 안정성이 높다.
- 협업/리뷰 설득이 쉽다.
단점도 있다.
- 응답 필드가 추가되면 타입을 수정해야 한다.
그래서 판단 기준은 단순하다.
- 정말 계속 빈 객체인가? ->
Record<string, never> - 곧 필드가 붙을 가능성이 큰가? -> 명시 DTO로 확장
결론
내 케이스(도면 삭제 응답)는 data: {}가 고정될 가능성이 높다.
그래서 SafetyDrawingDeleteResponseDtoType의 data는 Record<string, never>로 두는 것이 맞다고 판단했다.
핵심은 "지금 비어 있음"이 아니라 "비어 있어야 함"을 타입으로 표현하는 것이다.