39 / 58
Как создать тип DeepPartial<T>?
Полный ответ
Проблема со стандартным Partial:
TypeScript
interface Config {
db: {
host: string;
port: number;
credentials: {
user: string;
password: string;
};
};
cache: {
ttl: number;
maxSize: number;
};
}
// Partial делает опциональным только первый уровень
type ShallowPartial = Partial<Config>;
// { db?: { host: string; port: number; credentials: {...} }; cache?: {...} }
// Но db.host, db.port и т.д. по-прежнему обязательны!
Рекурсивная реализация:
TypeScript
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// Теперь все уровни опциональны
type PartialConfig = DeepPartial<Config>;
const override: PartialConfig = {
db: {
port: 5433,
// host, credentials — не обязательны
},
// cache — не обязателен
};
Более точная версия, которая не разворачивает массивы и Date:
TypeScript
type DeepPartial<T> = T extends Date | RegExp | Map<unknown, unknown> | Set<unknown>
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
Практическое применение — слияние конфигурации:
TypeScript
function deepMerge<T extends Record<string, unknown>>(
base: T,
override: DeepPartial<T>
): T {
const result = { ...base };
for (const key in override) {
const baseVal = base[key];
const overrideVal = override[key];
if (
typeof baseVal === "object" &&
baseVal !== null &&
typeof overrideVal === "object" &&
overrideVal !== null &&
!Array.isArray(baseVal)
) {
(result as Record<string, unknown>)[key] = deepMerge(
baseVal as Record<string, unknown>,
overrideVal as DeepPartial<Record<string, unknown>>
);
} else if (overrideVal !== undefined) {
(result as Record<string, unknown>)[key] = overrideVal;
}
}
return result;
}
const defaultConfig: Config = {
db: { host: "localhost", port: 5432, credentials: { user: "admin", password: "secret" } },
cache: { ttl: 3600, maxSize: 1000 },
};
const merged = deepMerge(defaultConfig, {
db: { port: 5433, credentials: { password: "new-secret" } },
}); // merged.db.host === "localhost" (из базового) // merged.db.port === 5433 (из override) // merged.db.credentials.password === "new-secret"
Реальные кейсы
Обновление состояния в Redux/Zustand:
TypeScript
interface AppState {
user: { name: string; settings: { theme: string; lang: string } };
notifications: { enabled: boolean; sound: boolean };
}
function updateState(state: AppState, patch: DeepPartial<AppState>): AppState {
return deepMerge(state, patch);
}
// Обновляем только тему — всё остальное не трогаем
updateState(state, { user: { settings: { theme: "dark" } } });
Ошибка: DeepPartial разворачивает массивы неправильно:
TypeScript
// ❌ Наивная версия превращает string[] в { [K in number]?: string }
type Naive<T> = { [K in keyof T]?: Naive<T[K]> };
type Bad = Naive<{ tags: string[] }>; // tags?: { [x: number]?: string } — сломано
// ✅ Правильная версия проверяет Array отдельно
type Good = DeepPartial<{ tags: string[] }>; // tags?: string[]
Резюме
DeepPartial<T> — рекурсивный mapped type, делающий опциональными свойства на всех уровнях вложенности. Добавьте проверки на Array, Date, Map, Set, чтобы не «разворачивать» их в mapped types. Основной кейс — мёрж конфигураций и частичное обновление глубоко вложенного состояния.