[ComitChu 개발기] TypeScript로 빈 객체를 표현하는 방법

Suyo·2025년 8월 4일
0

ComitChu

목록 보기
4/5

TypeScript에서 빈 객체를 타입으로 표현할 때 가장 흔히 사용되는 표기는 {}이다.
그러나 {}는 겉보기와 달리, 실제로는 빈 객체를 정확히 표현하는 타입이 아니다.

이 글에서는 {}Record<string, never>의 차이점을 정리하고,
정확하게 빈 객체를 표현하고자 할 때 어떤 방식이 더 적절한지를 설명한다.


1. {}는 모든 객체를 허용한다

const obj: {} = { a: 123 }; // 허용됨

TypeScript에서 {}모든 non-nullish 객체 타입을 허용하는 타입이다.

const a: {} = { foo: 'bar' }; // 허용
const b: {} = [];             // 허용
const c: {} = () => {};       // 허용
const d: {} = null;           // 오류
const e: {} = undefined;      // 오류

즉, {}는 빈 객체를 의미하지 않는다.
배열, 함수, 키가 존재하는 객체까지 모두 허용한다.

따라서 "빈 객체만 허용하고 싶다"는 의도를 전달하기에는 부족하다.


2. Record<string, never>가 진짜 빈 객체를 표현한다

type EmptyObject = Record<string, never>;

이 타입은 모든 문자열 키에 대해 값이 존재할 수 없는 객체를 뜻한다.
즉, 아무 키도 가질 수 없는 객체만 허용된다.

const a: Record<string, never> = {};         // 허용
const b: Record<string, never> = { foo: 1 }; // 오류

never인가?

  • Record<K, V>는 "K 키에 대해 V 값을 가지는 객체"를 의미한다.
  • never는 절대로 존재할 수 없는 타입이다.
  • 따라서 Record<string, never>는 "어떤 키도 존재할 수 없음"을 의미하며,
    완전히 비어 있는 객체만 통과시킨다.

3. 어떤 상황에서 사용하나?

1) 빈 객체를 명확히 제한할 때

function acceptsOnlyEmpty(obj: Record<string, never>) {
  // 어떤 키도 존재해서는 안 된다
}

acceptsOnlyEmpty({});           // 허용
acceptsOnlyEmpty({ foo: 'bar' }); // 오류

2) 조건부 타입에서 "남는 필드가 없음"을 표현할 때

type FilterString<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type A = { name: string; age: number };
type B = FilterString<A>; // { name: string }

type C = { id: number };
type D = FilterString<C>; // {} ≡ Record<string, never>

4. {}Record<string, never> 비교

표현의미키 허용 여부
{}모든 객체 허용키가 있어도 허용
Record<string, never>키가 없어야 함키가 하나라도 있으면 오류

5. Record<string, unknown>Record<string, any>의 차이

이 두 타입은 키가 존재해도 되며, 값의 타입 해석 방식이 다르다.

type A = Record<string, unknown>; // 값 타입은 unknown → 사용 전 검사 필요
type B = Record<string, any>;     // 값 타입은 any → 검사 없이 통과
타입특징
Record<string, never>키가 없어야 함
Record<string, unknown>키는 자유, 값은 타입 검사 필요
Record<string, any>키도 자유, 값도 제한 없음

6. 구조적 타입 호환: {}는 왜 Record<string, never>에 할당 가능한가?

type Result = {} extends Record<string, never> ? true : false;
// 결과는 true

이는 TypeScript의 구조적 타입 시스템 때문이며,
빈 객체는 "어떤 키도 없는 객체"로 간주되므로
Record<string, never>와 호환된다.

즉, {}는 타입으로는 느슨하지만,
값으로는 실제로 빈 객체일 경우 Record<string, never>와 호환된다.


7. 실전 예시

상태 초기화에서 사용

interface StringMap extends Record<string, string> {}

const cleared: Record<string, never> = {}; // 모든 키 제거 후 상태 초기화

API 응답에서 빈 객체 검사

type ApiResponse<T> = { data: T; error?: string };

function isEmpty<T>(res: ApiResponse<T>): res is ApiResponse<Record<string, never>> {
  return Object.keys(res.data).length === 0;
}

8. 정리

상황추천 타입
모든 객체 (배열, 함수 포함) 허용{}
진짜 빈 객체만 허용Record<string, never>
키는 자유, 값은 안전하게 처리Record<string, unknown>
키도 자유, 값도 자유롭게 처리Record<string, any>

마무리


TypeScript에서 빈 객체를 정확히 표현하고자 할 때는 {} 대신
Record<string, never>를 사용하는 것이 가장 안전한 방법이다.

  • {}는 실제로는 빈 객체가 아니며, 의도를 정확히 전달하지 못한다.
  • Record<string, never>는 타입 수준에서 "정말 아무 키도 없어야 한다"는 것을 보장한다.
  • 조건부 타입, 상태 초기화, API 응답 등 다양한 실전 상황에서 활용할 수 있다.
profile
Mee-

0개의 댓글