
TypeScript에서 제공하는 유틸리티 타입은 Partial, Pick, Omit, Record처럼 자주 사용되는 타입 조작 패턴을 간결하게 표현할 수 있습니다.
하지만 유틸리티 타입만으로는 부족한 경우도 있어요. 동적으로 키를 선택해야 하거나, 타입 제약이 필요한 함수를 만들 때는 제네릭(Generic)과 함께 사용하는 게 더 효과적입니다.
이번 글에서는 유틸리티 타입과 제네릭을 함께 사용하는 예제를 정리해보았어요.
Pick과 제네릭으로 안전한 속성 추출 함수 만들기
이 함수는 객체에서 필요한 필드만 뽑아서 새로운 객체로 만들어주는 유틸 함수입니다.
함수 시그니처를 살펴보면 T는 전체 객체의 타입이며 K extends keyof T는 선택 가능한 키를 제한하고 Pick<T, K>는 선택한 키만 포함된 부분 객체 타입을 생성합니다.
Pick<T, K>는 "T 타입에서 K 키만 남긴 새 타입을 만든다"는 의미로Pick<User, "name" | "email">은 아래와 같은 타입을 만들어요.// Pick<User, "name" | "email"> { name: string; email: string; }
또한 K extends keyof T 제약을 통해 존재하지 않는 키를 넘기면 컴파일 타임에 에러가 발생하도록 타입 안정성을 확보할 수 있어요.

Pick을 주로 사용하는 경우이처럼 제네릭을 활용하면 입력 객체와 추출 키에 따라 결과 타입이 정확히 추론되기 때문에 런타임 오류를 줄이면서도 재사용 가능한 함수를 만들 수 있습니다.
Partial과 제네릭으로 선택적 수정 함수 만들기
이 함수는 기존 객체에 일부 필드만 덮어써서 수정된 결과를 반환하는 유틸 함수입니다. Partial<T>는 타입 T의 모든 속성을 선택적(optional)으로 만들어주기 때문에 업데이트할 필드만 골라서 넘길 수 있습니다.

Partial을 주로 사용하는 경우이 함수는 제네릭을 활용하기 때문에 User, Product, Post 등 어떤 객체 타입에도 재사용할 수 있고 updates 객체에 존재하지 않는 필드를 넘기면 컴파일 타임에 타입 에러를 발생시켜 안전하게 막아줍니다.

Partial<T>와 제네릭 조합은 유연한 타입을 제공과 함께 실제 객체 조작에서 타입 안전성과 재사용성을 동시에 확보할 수 있는 실용적인 패턴입니다.
Record와 제네릭으로 키-값 매핑 객체 만들기
이 코드는 Status라는 문자열 리터럴 유니언 타입을 key로 모든 상태에 대응되는 메시지를 값으로 갖는 매핑 객체를 정의한 예시입니다.
여기서 사용한 Record<K, T>는 "K에 있는 모든 키를 갖고, 각 키의 값은 타입 T여야 한다"는 유틸리티 타입입니다.
즉 Record<Status, string>은 아래와 같은 타입을 의미합니다.
// Record<Status, string>
{
loading: string;
success: string;
error: string;
}
Record을 주로 사용하는 경우"REJECTED" → "요청이 반려되었습니다.")"admin" → true, "guest" → false)"price" → 가격 포맷터, "date" → 날짜 포맷터)이렇게 작성하면 각 키에 맞는 값을 반드시 정의해야 하고, 정의되지 않은 키를 사용하면 타입 오류가 발생해요.

Record<K, T>는 반복적인 매핑 구조를 정확하게 타입으로 보장하면서도 제네릭과 함께 사용하면 다양한 키 타입에 대해 확장성과 재사용성을 모두 갖춘 타입 안전한 매핑 도구를 만들 수 있습니다.
DeepPartial 제네릭으로 커스텀 유틸리티 타입 만들기사용자 정보처럼 중첩된 구조의 객체를 부분적으로 수정해야 할 때 기본 Partial<T>만으로는 부족할 수 있어요.
예를 들어 음과 같은 사용자 타입이 있다고 가정해보면, Partial<User>를 사용하면 profile 전체를 생략하거나 교체할 수는 있지만 contact.email만 선택해서 수정하려 하면 타입 오류가 발생합니다.
type User = {
id: number;
profile: {
name: string;
contact: {
email: string;
phone: string;
};
};
};
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
Partial<T>는 객체의 바로 아래 단계 필드만 선택적으로 바꿀 수 있지만, DeepPartial<T>는 객체 안의 중첩된 필드까지 모두 선택적으로 바꿀 수 있습니다.
이런 구조는 깊이 있는 객체를 수정할 때 필요한 필드만 안전하게 선택할 수 있도록 타입을 설계하는 데 유용합니다.

이렇게 작성하면 contact.email만 수정하고 싶은 상황에서도 타입 에러 없이 필요한 필드만 안전하게 넘길 수 있어요.
Partial<T>와 차이
Partial<T>를 사용하면 profile 속성 자체는 생략할 수 있지만
profile 안의 name과 email은 여전히 필수입니다.
React Hook Form, Zod, Yup 등에서 초기값(defaultValues) 설정 시이처럼 객체를 전체 교체하기보다는, 깊이 있는 구조 중 일부만 수정해야 하는 경우가 많기 때문에 DeepPartial<T> 같은 커스텀 타입 유틸은 실무에서 유용하게 사용할 수 있습니다.
이처럼 Partial<T>, Pick<T, K>, Record<K, T> 같은 유틸리티 타입은 제네릭과 함께 조합해 사용하면 더 강력하고 유연한 타입 설계를 할 수 있습니다.
이번에 유틸리티 타입과 제네릭을 함께 정리해보면서, 실무에서 마주했던 타입 구조나 에러 상황을 더 구조적이고 깔끔하게 정리할 수 있었겠다는 생각이 들었습니다.
앞으로도 복잡한 타입 설계가 필요할 때 제네릭과 유틸리티 타입을 잘 활용해보고 싶어요. 💪🏻
이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻