
라이브러리를 개발하다 보면 객체 타입의 구조는 그대로 유지하면서, 일부 프로퍼티의 값 타입만 교체해야 하는 상황이 자주 발생한다. 특히 상속이나 오버라이드 패턴을 사용해 메서드의 동작 일부를 변경하는 경우가 대표적이다. 구현 관점에서는 기존 로직을 재사용하되, 외부에 노출되는 인터페이스만 달라지는 형태다.
타입스크립트에서는 이러한 요구를 충족하기 위해 보통 Omit, Pick, Partial 같은 기본 유틸리티 타입과 인터섹션 타입을 조합한다. 예를 들어, 특정 메서드의 시그니처만 교체하고 싶은 경우에는 다음과 같은 타입을 작성하게 된다.
type ReducerStore = Omit<Store<T>, "setStore"> & {
setStore: Dispatcher;
};이 코드는 기능적으로 아무 문제가 없다. 실제로 타입스크립트 커뮤니티에서도 널리 사용되는 패턴이다. 다만 한 가지 아쉬운 점이 있다. 이 타입이 무엇을 의도하고 있는지 한눈에 파악하기 어렵다는 것이다. Omit과 & 연산자를 해석해야만 “아, 기존 타입에서 특정 프로퍼티의 타입을 교체하려는 거구나”라는 의도를 유추할 수 있다.
이 문제를 해결하기 위해 나는 라이브러리에 ReplacePropertyValue라는 유틸리티 타입을 도입했다.
type ReplacePropertyValue<
T extends object,
U extends { [K in keyof T]?: unknown }
> = Omit<T, keyof U> & U;이 타입이 하는 일은 매우 단순하다. 기존 타입 T에서 U에 포함된 키들을 제거한 뒤, 다시 U와 병합한다. 즉, 결과적으로는 T의 특정 프로퍼티 값 타입만 U에 정의된 타입으로 교체하는 구조다. 동작만 놓고 보면, 앞서 작성했던 Omit + & 패턴과 정확히 동일하다.
그럼에도 불구하고 이 타입을 따로 정의한 이유는 명확하다. 이 타입은 “어떻게”보다 “무엇을” 드러낸다. ReplacePropertyValue라는 이름 자체가, 이 타입이 기존 구조를 유지한 채 일부 프로퍼티의 값 타입만 바꿔치기한다는 의도를 그대로 전달한다. 별도의 주석이 없어도, 타입 선언만 보고 충분히 맥락을 이해할 수 있다. 이를 기존 코드에 적용하면 다음과 같이 정리할 수 있다.
type ReducerStore =
Omit<Store<T>, "setStore"> & { setStore: Dispatcher };
type ReducerStore =
ReplacePropertyValue<Store<T>, { setStore: Dispatcher }>;두 타입은 완전히 동일하게 동작한다. 하지만 두 번째 표현은 타입을 읽는 사람에게 훨씬 친절하다. 이 코드를 처음 접하는 개발자도 “아, 이 타입은 Store<T>에서 setStore의 타입만 교체하는구나”라고 즉시 이해할 수 있다.
이처럼 커스텀 유틸리티 타입은 타입스크립트의 기능을 확장하기보다는, 의도를 드러내기 위해 존재하는 경우가 많다. 특히 라이브러리 코드처럼 여러 사람이 읽고 유지보수해야 하는 환경에서는, 타입의 가독성과 의미 전달력이 곧 코드 품질로 이어진다. ReplacePropertyValue는 그 점에서, 작은 비용으로 큰 효과를 얻을 수 있는 유틸리티 타입이라고 생각한다.
더 읽어보기
2025.09.14
Loose Autocomplete
프로그래밍을 하다 보면 개발자가 의도한 선택지를 에디터가 자동완성으로 얼마나 잘 안내해 주느냐가 개발 경험에 큰 영향을 미친다는 사실을 자주 체감하게 된다. 특히 문자열 기반의 옵션을 다룰 때는 이 차이가 더욱 극명하게 드러난다. 예를 들어 HTTP 메서드를 표현할 때 "GET" | "…
2025.04.25
더 좁은 타입의 유효성에 대하여
사람이 무언가를 집중해서 바라보다 보면, 어느샌가 주변부가 흐려지고 가끔은 집중하고 있던 그 대상조차 보이지 않게 된다. 처음엔 분명하게 인식되던 경계가 서서히 사라지고, 오히려 애써 무시했던 주변이 본질을 가릴 때도 있다. 잘 보려 애쓰는 행위가, 역설적으로 시야를 좁히는 순간이다.…
2025.04.06
재귀 조건부 타입에서의 추론 컨텍스트 손실
타입스크립트에서는 조건부 타입의 분배 과정에서도 타입 추론 컨텍스트가 유지된다. 이 특성 덕분에 단순한 분기 수준을 넘어, 상당히 복잡한 조건부 타입에서도 개발자가 의도한 방향으로 타입 추론을 유도할 수 있다. 실제로 이러한 특성은 고급 유틸리티 타입을 설계할 때 매우 강력한 도구로 작…
2025.03.28
유틸리티 타입 IsInRange
프론트엔드 개발을 하다 보면 컴포넌트의 props로 number 타입을 받을 때가 많다. 하지만 단순히 number 타입만 지정하면 값의 범위를 제한할 수 없다는 점이 아쉽다. 예를 들어, 페이지네이션의 currentPage는 1 이상의 값만 허용해야 하고, 슬라이더의 value는 최소…
2026.06.01
React Server Components를 위한 컴포넌트 아키텍처
이 포스트는 Vercel의 Next.js 팀 소속 개발자 Aurora Scharff가 자신의 블로그에 올린 Component Architecture for React Server Components 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
댓글
댓글을 불러오는 중...