PUBLISHED

유틸리티 타입 ReplacePropertyValue

작성일: 2025.01.10

유틸리티 타입 ReplacePropertyValue

라이브러리를 개발하다 보면 객체 타입의 구조는 그대로 유지하면서, 일부 프로퍼티의 값 타입만 교체해야 하는 상황이 자주 발생한다. 특히 상속이나 오버라이드 패턴을 사용해 메서드의 동작 일부를 변경하는 경우가 대표적이다. 구현 관점에서는 기존 로직을 재사용하되, 외부에 노출되는 인터페이스만 달라지는 형태다.

타입스크립트에서는 이러한 요구를 충족하기 위해 보통 Omit, Pick, Partial 같은 기본 유틸리티 타입과 인터섹션 타입을 조합한다. 예를 들어, 특정 메서드의 시그니처만 교체하고 싶은 경우에는 다음과 같은 타입을 작성하게 된다.

untitled
TS
type ReducerStore = Omit<Store<T>, "setStore"> & {
  setStore: Dispatcher;
};

이 코드는 기능적으로 아무 문제가 없다. 실제로 타입스크립트 커뮤니티에서도 널리 사용되는 패턴이다. 다만 한 가지 아쉬운 점이 있다. 이 타입이 무엇을 의도하고 있는지 한눈에 파악하기 어렵다는 것이다. Omit과 & 연산자를 해석해야만 “아, 기존 타입에서 특정 프로퍼티의 타입을 교체하려는 거구나”라는 의도를 유추할 수 있다.

이 문제를 해결하기 위해 나는 라이브러리에 ReplacePropertyValue라는 유틸리티 타입을 도입했다.

untitled
TS
type ReplacePropertyValue<
  T extends object,
  U extends { [K in keyof T]?: unknown }
> = Omit<T, keyof U> & U;

이 타입이 하는 일은 매우 단순하다. 기존 타입 T에서 U에 포함된 키들을 제거한 뒤, 다시 U와 병합한다. 즉, 결과적으로는 T의 특정 프로퍼티 값 타입만 U에 정의된 타입으로 교체하는 구조다. 동작만 놓고 보면, 앞서 작성했던 Omit + & 패턴과 정확히 동일하다.

그럼에도 불구하고 이 타입을 따로 정의한 이유는 명확하다. 이 타입은 “어떻게”보다 “무엇을” 드러낸다. ReplacePropertyValue라는 이름 자체가, 이 타입이 기존 구조를 유지한 채 일부 프로퍼티의 값 타입만 바꿔치기한다는 의도를 그대로 전달한다. 별도의 주석이 없어도, 타입 선언만 보고 충분히 맥락을 이해할 수 있다. 이를 기존 코드에 적용하면 다음과 같이 정리할 수 있다.

untitled
TS
type ReducerStore =
  Omit<Store<T>, "setStore"> & { setStore: Dispatcher };

type ReducerStore =
  ReplacePropertyValue<Store<T>, { setStore: Dispatcher }>;

두 타입은 완전히 동일하게 동작한다. 하지만 두 번째 표현은 타입을 읽는 사람에게 훨씬 친절하다. 이 코드를 처음 접하는 개발자도 “아, 이 타입은 Store<T>에서 setStore의 타입만 교체하는구나”라고 즉시 이해할 수 있다.

이처럼 커스텀 유틸리티 타입은 타입스크립트의 기능을 확장하기보다는, 의도를 드러내기 위해 존재하는 경우가 많다. 특히 라이브러리 코드처럼 여러 사람이 읽고 유지보수해야 하는 환경에서는, 타입의 가독성과 의미 전달력이 곧 코드 품질로 이어진다. ReplacePropertyValue는 그 점에서, 작은 비용으로 큰 효과를 얻을 수 있는 유틸리티 타입이라고 생각한다.