Partial<T>는 T의 모든 속성을 선택적으로 만드는 내장 유틸리티 타입입니다. 호출하는 쪽이 바뀐 필드만 보내는 업데이트나 패치에 자연스럽게 맞는 타입입니다.
Partial<User>는 { id?: number; name?: string; email?: string }입니다. 컴파일러는 넘긴 필드는 여전히 검사하므로 { nmae: "x" }나 { name: 42 }는 오류입니다. 이것이 느슨한 object나 any 매개변수보다 Partial이 나은 이유입니다.
Partial의 정의
Partial은 TypeScript 표준 라이브러리에 있는 한 줄짜리 매핑된 타입입니다.
type Partial<T> = {
[P in keyof T]?: T[P];
};
T의 각 키 P에 대해 같은 타입의 선택적 속성을 선언합니다. keyof T를 매핑하므로 원래 readonly였던 속성은 readonly로 유지됩니다. 평범한 타입이라 런타임 효과는 없습니다. changes로 넘긴 객체는 어느 쪽이든 같은 객체입니다.
Partial에서 읽으면 T | undefined가 된다
Partial<T>의 모든 속성은 없을 수 있으므로, 읽으면 속성 타입에 undefined가 더해집니다. 컴파일러는 없는 경우를 처리하도록 요구합니다.
{ ...defaults, ...opts }는 Partial<Options>를 다시 완전한 Options로 만드는 흔한 방법입니다. 기본값을 먼저 펼치고, 넘어온 값이 그것을 덮어쓰게 합니다.
명시적 undefined 함정
선택적 속성은 없을 수도 있지만 undefined 값으로 존재할 수도 있습니다. 객체 스프레드는 그 undefined를 실제 값 위에 복사하고, 결과 타입에는 그것이 드러나지 않습니다.
빈 필드가 undefined가 되는 폼이나 쿼리 문자열로 패치를 만들 때 이 문제가 생깁니다. exactOptionalPropertyTypes 컴파일러 옵션(strict에 포함되지 않음)을 켜면 name?: string | undefined라고 쓰지 않는 한 name?: string에 대한 { name: undefined }가 컴파일 오류가 되어 원천적으로 막아 줍니다.
Partial은 얕다
Partial은 최상위 속성만 선택적으로 만듭니다. 중첩 객체를 넘긴다면 그 객체는 완전해야 합니다.
index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.
editor 속성은 선택적이지만, 일단 있으면 타입은 그대로 { fontSize: number; tabSize: number }입니다. 설정 객체, API 패치, 테스트 픽스처에서는 모든 단계의 속성이 선택적이기를 바라는 경우가 많습니다. 그러려면 재귀 타입이 필요합니다.
DeepPartial: 재귀적인 Partial
TypeScript에는 깊게 적용되는 내장 버전이 없지만 몇 줄이면 만들 수 있습니다. 배열 요소를 선택적으로 만들면 [undefined]가 허용되므로 함수와 배열은 그대로 둡니다.
타입은 재귀적이지만 병합은 그렇지 않습니다. 객체 스프레드도 얕기 때문에 applySettings는 editor를 직접 병합합니다. 범용 깊은 병합 함수는 lodash(merge) 같은 라이브러리에 있으며, 그 타입 지정은 위의 타입보다 어렵습니다.
Required: Partial의 반대
Required<T>는 모든 속성에서 ?를 제거합니다. -? 수정자로 정의되며, 이 수정자는 각 속성 타입에서 undefined도 제거합니다.
패턴은 Partial과 같고 방향만 반대입니다. API 사용자는 느슨한 설정을 넘기고, 내부 코드는 모든 값이 존재한다고 알려진 Required 버전으로 작업합니다. 이 스프레드에도 위의 업데이트 함수와 같은 구멍이 있습니다. 호출하는 쪽이 port: undefined를 명시적으로 넘기면 기본값이 undefined로 덮어써지고, exactOptionalPropertyTypes가 꺼져 있으면 컴파일러는 이를 받아들입니다. Required도 Partial과 같은 방식으로 얕습니다.
일부 속성만 선택적 또는 필수로 만들기
Partial과 Required는 모든 속성에 적용됩니다. 몇 개만 바꾸려면 Pick과 Omit으로 타입을 나눈 뒤 다시 합칩니다.
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Post {
id: number;
title: string;
body?: string;
}
type NewPost = PartialBy<Post, "id">; // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
| 타입 | 효과 | 깊게 적용? |
|---|---|---|
Partial<T> | 모든 속성을 선택적으로 | 아니요 |
Required<T> | 모든 속성을 필수로, undefined 제거 | 아니요 |
DeepPartial<T> (직접 작성) | 모든 단계에서 선택적으로 | 예 |
PartialBy<T, K> (직접 작성) | 키 K만 선택적으로 | 아니요 |
Readonly<T> | 모든 속성을 readonly로 | 아니요 |
자주 묻는 질문
TypeScript에서 Partial은 무엇을 하나요?
Partial<T>는 T의 모든 속성을 선택적으로 표시한 타입을 만듭니다. interface User { name: string; email: string }에 대해 Partial<User>는 { name?: string; email?: string }이므로 {}, { name: "Ada" }, 완전한 사용자 모두 유효한 값입니다.
TypeScript의 Partial은 깊게 적용되나요?
아니요. Partial은 최상위 속성에만 영향을 줍니다. Partial<T> 안의 중첩 객체는 여전히 완전해야 합니다. 재귀적으로 적용하려면 객체 타입 속성에 자기 자신을 적용하는 DeepPartial<T> 타입을 직접 작성하세요.
TypeScript에서 Partial의 반대는 무엇인가요?
Required<T>입니다. 모든 속성에서 ?를 제거하고 타입에서 undefined도 제거하므로 Required<{ port?: number }>는 { port: number }입니다. -? 수정자를 쓴 매핑된 타입으로 정의되어 있습니다.
일부 속성만 선택적으로 만들려면 어떻게 하나요?
Omit, Pick, Partial을 조합합니다: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>. PartialBy<User, "email">은 email만 선택적으로 바꾸고 나머지 속성은 그대로 둡니다.
Partial 업데이트를 병합한 뒤 속성이 undefined가 되는 이유는 무엇인가요?
Partial<T>는 속성이 undefined 값으로 존재하는 것을 허용하고, 객체 스프레드는 그 값을 복사합니다. TypeScript가 결과를 User로 타입 지정하더라도 { ...user, ...{ name: undefined } }의 name은 undefined입니다. 병합 전에 undefined 값을 걸러내거나, exactOptionalPropertyTypes를 켜서 명시적 undefined를 거부하세요.