함수 파라미터에서 옵셔널 값을 디폴트 값으로 처리하는 것은 일반적인 패턴입니다. 디폴트 값은 개발자가 명시적으로 정의하지만, 타입은 여전히 옵셔널로 남아있어 타입 안전성이 보장되지 않습니다. 타입이 변경되거나 추가될 때 디폴트 값이 제대로 반영되지 않을 수 있습니다.
디폴트 값의 타입을 파라미터 타입의 모든 속성을 Required 처리하여 정의하면, 이러한 불일치를 컴파일 타임에 감지할 수 있습니다.
TypeScript는 내장 타입으로 Required<T> 타입을 제공합니다. 하지만 이 타입은 인터페이스의 최상위 프로퍼티에 대해서만 Required 처리를 수행합니다. 중첩된 객체의 옵셔널 속성은 그대로 유지되어 완전한 타입 안전성을 보장하지 못합니다.
인터페이스 내의 모든 타입에 대해 재귀적으로 Required 처리를 수행하는 타입을 구현할 수 있습니다:
// Required 처리가 불필요한 내장 타입들을 제외합니다.
// 프로젝트 요구사항에 따라 추가 타입을 정의할 수 있습니다.
type ExcludeType = Element | Window | NodeList | HTMLCollection;
// 1. object가 아닌 프리미티브 값인 경우 해당 값을 그대로 반환
// 2. ExcludeType에 속하는 경우 원래 타입을 그대로 반환
// 3. 객체인 경우 자기 자신을 다시 호출하여 재귀적으로 Required 처리
type RequiredParams<T> = T extends object
? {
[K in keyof T]-?: T[K] extends infer U
? U extends ExcludeType
? U
: RequiredParams<U>
: T[K];
}
: T;-? 연산자: 옵셔널 수식어를 제거하여 속성을 필수로 변환합니다.infer U: 타입을 추론하여 조건부 타입 검사를 수행합니다.RequiredParams<U>를 다시 호출하여 모든 깊이의 옵셔널 속성을 처리합니다.interface Params {
name?: {
last?: string;
first: string;
};
}
type Required = RequiredParams<Params>;
const result: Required = {
// [주의] last 속성이 없으므로 컴파일 에러 발생
name: {
first: 'first',
},
};
const validResult: Required = {
// [권장] 모든 속성이 필수로 정의됨
name: {
last: 'last',
first: 'first',
},
};RequiredParams 타입을 적용하면 name과 last 속성의 옵셔널이 모두 제거됩니다. 이를 통해 디폴트 값 정의 시 타입 불일치를 컴파일 타임에 감지할 수 있습니다.
interface Config {
server?: {
port?: number;
host?: string;
};
}
type CompleteConfig = RequiredParams<Config>;
// 디폴트 값은 모든 속성을 포함해야 합니다.
const defaultConfig: CompleteConfig = {
server: {
port: 3000,
host: 'localhost',
},
};
function createConfig(config?: Config): CompleteConfig {
return { ...defaultConfig, ...config };
}이 패턴을 사용하면 디폴트 값 정의 시 누락된 속성을 컴파일 타임에 발견할 수 있습니다.
TypeScript의 내장 Required<T> 타입은 최상위 속성만 처리하므로, 중첩된 객체의 완전한 타입 안전성을 보장하지 못합니다. 재귀적 RequiredParams<T> 타입을 구현하면 모든 깊이의 옵셔널 속성을 필수로 변환할 수 있습니다.
이러한 유틸리티 타입은 런타임 에러를 컴파일 타임에 감지하여 코드의 안정성과 예측 가능성을 향상시킵니다.
TypeScript의 고급 타입 시스템과 제네릭을 활용한 확장 가능한 팩토리 패턴 설계 가이드
Enum, Const Enum, 객체 리터럴의 컴파일 동작 비교와 최적의 선택 가이드