TypeScript에서 빈 객체를 타입으로 표현할 때 가장 흔히 사용되는 표기는 {}
이다.
그러나 {}
는 겉보기와 달리, 실제로는 빈 객체를 정확히 표현하는 타입이 아니다.
이 글에서는 {}
와 Record<string, never>
의 차이점을 정리하고,
정확하게 빈 객체를 표현하고자 할 때 어떤 방식이 더 적절한지를 설명한다.
{}
는 모든 객체를 허용한다const obj: {} = { a: 123 }; // 허용됨
TypeScript에서 {}
는 모든 non-nullish 객체 타입을 허용하는 타입이다.
const a: {} = { foo: 'bar' }; // 허용
const b: {} = []; // 허용
const c: {} = () => {}; // 허용
const d: {} = null; // 오류
const e: {} = undefined; // 오류
즉, {}
는 빈 객체를 의미하지 않는다.
배열, 함수, 키가 존재하는 객체까지 모두 허용한다.
따라서 "빈 객체만 허용하고 싶다"는 의도를 전달하기에는 부족하다.
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>
는 "어떤 키도 존재할 수 없음"을 의미하며,function acceptsOnlyEmpty(obj: Record<string, never>) {
// 어떤 키도 존재해서는 안 된다
}
acceptsOnlyEmpty({}); // 허용
acceptsOnlyEmpty({ foo: 'bar' }); // 오류
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>
{}
와 Record<string, never>
비교표현 | 의미 | 키 허용 여부 |
---|---|---|
{} | 모든 객체 허용 | 키가 있어도 허용 |
Record<string, never> | 키가 없어야 함 | 키가 하나라도 있으면 오류 |
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> | 키도 자유, 값도 제한 없음 |
{}
는 왜 Record<string, never>
에 할당 가능한가?type Result = {} extends Record<string, never> ? true : false;
// 결과는 true
이는 TypeScript의 구조적 타입 시스템 때문이며,
빈 객체는 "어떤 키도 없는 객체"로 간주되므로
Record<string, never>
와 호환된다.
즉, {}
는 타입으로는 느슨하지만,
값으로는 실제로 빈 객체일 경우 Record<string, never>
와 호환된다.
interface StringMap extends Record<string, string> {}
const cleared: Record<string, never> = {}; // 모든 키 제거 후 상태 초기화
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;
}
상황 | 추천 타입 |
---|---|
모든 객체 (배열, 함수 포함) 허용 | {} |
진짜 빈 객체만 허용 | Record<string, never> |
키는 자유, 값은 안전하게 처리 | Record<string, unknown> |
키도 자유, 값도 자유롭게 처리 | Record<string, any> |
TypeScript에서 빈 객체를 정확히 표현하고자 할 때는{}
대신
Record<string, never>
를 사용하는 것이 가장 안전한 방법이다.
{}
는 실제로는 빈 객체가 아니며, 의도를 정확히 전달하지 못한다.Record<string, never>
는 타입 수준에서 "정말 아무 키도 없어야 한다"는 것을 보장한다.- 조건부 타입, 상태 초기화, API 응답 등 다양한 실전 상황에서 활용할 수 있다.