반응형

TypeScript를 쓰다 보면 Partial, Pick, Omit, Record 같은 유틸리티 타입을 자주 만나게 됩니다. 처음에는 “기존 타입을 재사용해서 코드를 줄이는 문법”처럼 보이지만, 실무에서는 그보다 더 중요한 역할을 합니다.

바로 API 요청값, 폼 입력값, 서버가 관리하는 필드처럼 데이터가 오가는 경계를 안전하게 나누는 일입니다. 타입을 어떻게 설계하느냐에 따라 클라이언트가 보내면 안 되는 값이 자연스럽게 막히기도 하고, 반대로 의도치 않게 허용되기도 합니다.

TypeScript Utility Types가 중요한 이유

유틸리티 타입은 기존 타입을 바탕으로 새로운 타입을 만드는 도구입니다. 다만 실무에서는 단순히 중복을 줄이는 데서 끝나지 않습니다. 어떤 필드를 외부 입력으로 받을지, 어떤 필드는 서버가 관리해야 하는지 구분하는 기준이 됩니다.

특히 수정 API나 폼 데이터 타입을 만들 때는 “편한 타입”보다 “안전한 타입”을 우선해야 합니다.

Partial: 모든 필드를 선택적으로 만든다

Partial<T>는 타입 T의 모든 프로퍼티를 선택적으로 바꿉니다.

type User = {
  id: string;
  name: string;
  email: string;
  role: "admin" | "member";
  createdAt: string;
};

type UpdateUser = Partial<User>;

이렇게 만들면 name, email뿐 아니라 id, role, createdAt까지 모두 선택적으로 허용됩니다.

수정 API에서는 이 점이 문제가 될 수 있습니다. 사용자가 이름이나 이메일만 수정해야 하는 상황인데, 타입상으로는 서버가 관리해야 할 id, role, createdAt 같은 값도 payload에 들어갈 수 있기 때문입니다.

즉 Partial<User>는 편하지만 범위가 넓습니다.

Pick과 함께 쓰면 입력 범위를 좁힐 수 있다

수정 가능한 필드가 정해져 있다면 Partial<User>를 그대로 쓰기보다 Partial<Pick<User, ...>> 형태가 더 안전합니다.

type UpdateUserRequest = Partial<Pick<User, "name" | "email">>;

function updateUser(id: User["id"], payload: UpdateUserRequest) {
  return {
    id,
    ...payload,
  };
}

이 타입은 name, email만 허용하면서도 둘 중 일부만 보낼 수 있게 만듭니다.

여기서 Pick은 허용할 필드만 고르고, Partial은 고른 필드를 선택적으로 만드는 역할을 합니다. 그래서 외부에서 들어오는 요청 타입에는 Partial<Pick<...>> 조합이 자주 어울립니다.

Pick vs Omit: 허용 목록과 제외 목록의 차이

Pick은 allow-list 방식이고, Omit은 deny-list 방식에 가깝습니다.

type SafeUpdateUserRequest = Partial<Pick<User, "name" | "email">>;

type RiskyUpdateUserRequest = Partial<Omit<User, "id" | "createdAt">>;

Omit은 현재 제외한 필드만 빠집니다. 당장은 괜찮아 보여도 나중에 status, lastLoginAt, updatedAt 같은 서버 관리 필드가 User에 추가되면, 그 필드가 자동으로 요청 타입에 포함될 수 있습니다.

반면 Pick은 처음부터 허용한 필드만 유지합니다. 도메인 타입이 커져도 외부 입력 타입의 범위가 함부로 넓어지지 않습니다.

Record: 정해진 key 전체를 매핑할 때 쓴다

Record<K, V>는 특정 key 집합에 대해 value 타입을 강제할 때 사용합니다.

type UserStatus = "active" | "invited" | "blocked";

const statusLabel: Record<UserStatus, string> = {
  active: "활성",
  invited: "초대됨",
  blocked: "차단됨",
};

Record<UserStatus, string>은 active, invited, blocked 키가 모두 있어야 하고, 각 값은 string이어야 한다는 뜻입니다.

상태값과 라벨, 권한과 메뉴, 라우트와 설정처럼 정해진 키 전체를 빠짐없이 매핑해야 할 때 유용합니다. 만약 UserStatus에 새 상태가 추가되면 매핑 객체에서도 빠진 값을 타입 에러로 알려줍니다.

실무 판단 기준

유틸리티 타입을 고를 때는 “코드를 얼마나 짧게 쓸 수 있나”보다 “어떤 데이터가 경계를 넘을 수 있나”를 먼저 봐야 합니다.

  • 외부 입력값이라면 가능한 한 Pick 기반 allow-list로 좁게 설계합니다.
  • 내부 타입 재사용이라면 Omit도 편리하게 쓸 수 있습니다.
  • 일부 필드만 수정 가능해야 한다면 Partial<Pick<...>>를 우선 고려합니다.
  • 정해진 키 전체를 빠짐없이 매핑해야 한다면 Record를 사용합니다.

마무리

TypeScript Utility Types는 단순한 문법 편의 기능이 아닙니다. 잘 쓰면 변경이 들어와도 잘못된 데이터가 API나 폼 경계를 넘지 못하게 막아주는 설계 장치가 됩니다.

오늘의 핵심은 이 한 문장으로 정리할 수 있습니다.

Partial<User>는 편하지만 넓고, Partial<Pick<User, "name" | "email">>은 좁고 안전합니다.

반응형