재밌는 유틸타입 고민..

houndhollis·2025년 8월 29일
6

남기고 싶은 소소한 유틸타입에 대하여,

Props 정의 중 아래 보이시는 로직을 작성을 하다가 고민을 한 적이 있습니다.
(조금 다른 예시 코드로 작성함에 따라 참고차 봐주셨으면 합니다)

type User = "id" | "email" | "nickName";

interface Props {
  // 기타 다른 타입
  user : Record<User, string>
}

크게 이상할 것이 없는데? 라고 생각할 수 있습니다. 하지만 제가 원했던 것은

User type 은 id 값은 (required) 이며, email과 nickName은 (optional) 해야합니다.

음.. 그러면 어떻게 작성하는 게 좋을까요?

type User = {
  id: string
  email?: string
  nickName?: string
}

interface Props {
  user: User
}

뭔가 상당히 불만족스러웠습니다. 아! 뭔가 다른 방법이 있지 않을까? 하면서 작성했던 첫 번째 코드가 있습니다.

type MakeRequiredKey<T extends PropertyKey, Req extends T, V> = 
{ [P in Req]: V } & { [P in Exclude<T, Req>]?: V };

// 위와 같이 유틸타입을 만들어서 

type User = "id" | "email" | "nickName";
type UserMap = MakeRequiredKey<User, "id", string>;

interface Props {
  user: UserMap;
}

// 작성을 해주었습니다, 하지만 여기서 든 생각이 너무 장황한가? 알아보기 힘들려나.. 하고 새롭다 다시 작성한 것이 아래 로직과 같습니다.

type MakeRequiredKey2<T extends PropertyKey, Req extends T, V> =
  Partial<Record<T, V>> & // 전체를 optional
  Record<Req, V>;         // Req는 다시 필수

MakeRequiredKey2 로직의 경우 자주 쓰이는 Partial 과 Record를 써서 간결하게 작성도 해봤습니다.
여기 큰 문제는 MakeRequiredKey2의 경우 아래 사진과 같이

Partial<Record<...>> & Record<...> 그대로 드러납니다.

기존 MakeRequiredKey 의 경우는 맵드 타입을 펼친 형태라 IDE에서 hover시

{
  id: string;       // 필수
  name?: string;    // 선택
  email?: string;   // 선택
}

처럼 보여지는 것을 확인할 수 있었습니다.

🧐 두 타입 모두 동일한 결과를 나타내지만 간결하게 유틸 타입을 만들어서, 명확하게 알아보는 게 좋을까?
아니면 hover시 타입 추론이 잘 되어서 명시적으로 보는 게 좋을까? 라는 고민과 함께 발견한 타입이 바로

심플리파이(simplify)

교차 타입(&) 같은 게 있으면 IDE에서 그대로 노출돼서 지저분하게 보이는데, Simplify 같은 유틸리티를 써주면 한 번 매핑을 거쳐 최종 타입을 평탄화(flatten) 해줄 수 있었습니다.

type Simplify<T> = { [K in keyof T]: T[K] } & {};
  
type MakeRequiredKey2<T extends PropertyKey, Req extends T, V> =
 Partial<Record<T, V>> & Record<Req, V>;    
 
// 아래 처럼 작성 했을경우, IDE에서 hover했을경우, 타입추론도 잘 되는 모습을 볼 수 있었습니다.
type UserMap = Simplify<MakeRequiredKey<User, "id", string>>;

한가지더..

여기서 저는 한 가지 더 아쉬운 점이 있었습니다. 바로 V인 value의 타입이 강제되는 현상인데요, 그래서 고안한 방법이

type MakeRequiredKey<
  T extends PropertyKey,
  Req extends T,
  V
> = V extends Record<T, any>
  ? { [P in Req]-?: V[P] } & { [P in Exclude<T, Req>]?: V[P] }
  : { [P in Req]-?: V } & { [P in Exclude<T, Req>]?: V };

사용예시

type User = "id" | "email" | "nickName";

// V가 객체 맵핑일 때
type UserMap1 = MakeRequiredKey<User, "id", { id: string; email: number; nickName: boolean }>;

/* 결과:
{
  id: string;
  email?: number;
  nickName?: boolean;
}
*/

// V가 단일 타입일 때
type UserMap2 = MakeRequiredKey<User, "id", string>;

/* 결과:
{
  id: string;
  email?: string;
  nickName?: string;
}
*/

물론 실제 작성에서는 Simplify 부분도 적용을 해봤습니다.
여기까지 오면서 타입을 단순 선언에서 유틸 타입으로 확장하고, 다시 IDE 친화적인 형태로 다듬는 과정을 경험했습니다. 작지만 이런 시도들이 쌓이면 점점 더 '읽기 좋은 타입'을 만드는 감각이 생기는 것 같습니다.

profile
한 줄 소개

5개의 댓글

comment-user-thumbnail
2025년 9월 3일

이상한게 없는데? 생각했는데 설득되네요 ㅎㅎ
평탄화 하는 것은 몰랐는데 새롭게 알아갑니다! 📝📝📝

답글 달기
comment-user-thumbnail
2025년 9월 3일

사실 저는 기본적으로 제공되는 유틸 타입 혹은 커스텀 타입으로만 사용했었는데 이렇게 커스텀 유틸 타입을 만들어서 사용하는 것이 더 타입스크립트를 잘 사용하는 방법 중 하나라고 생각이 들었습니다!
덕분에 타입 평탄화에 대해서도 알게 되었습니다!

답글 달기
comment-user-thumbnail
2025년 9월 3일

타입 시리즈 너무 재밌습니다!
예시 코드들 덕분에 흐름을 따라가는데에 많은 도움이 됩니다!

답글 달기
comment-user-thumbnail
2025년 9월 8일

호버에서 타입이 제대로 보이는지 아닌지가 저한텐 굉장히 중요한 문제였는데, 괜찮은 팁도 얻어가네요!!

답글 달기
comment-user-thumbnail
2025년 9월 16일

타입 스크립트를 쓰면서, 타입에 대해 이렇게까지 고민해본 적은 없는 것 같아 조금 반성하고 갑니다. 잘 보고 갑니다!

답글 달기