tech

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: {}가 고정될 가능성이 높다.
그래서 SafetyDrawingDeleteResponseDtoTypedataRecord<string, never>로 두는 것이 맞다고 판단했다.

핵심은 "지금 비어 있음"이 아니라 "비어 있어야 함"을 타입으로 표현하는 것이다.