31. Partial, Required, Readonly
TypeScript: Броня. Урок 30: Partial, Required, Readonly
Заголовок раздела «TypeScript: Броня. Урок 30: Partial, Required, Readonly»Partial, Required и Readonly - это встроенные utility types TypeScript, которые трансформируют свойства объектных типов. Они являются основой для создания гибких и type-safe API, особенно полезны при работе с формами, конфигурациями и частичными обновлениями данных.
Partial<T>
Заголовок раздела «Partial<T>»Partial<T> делает все свойства типа T optional:
// Определение Partial (встроенное)type Partial<T> = { [P in keyof T]?: T[P];};
// Пример использованияinterface User { id: string; name: string; email: string; age: number;}
// Все поля опциональныtype PartialUser = Partial<User>;// {// id?: string;// name?: string;// email?: string;// age?: number;// }
// Практическое применение: partial updatefunction updateUser(id: string, updates: Partial<User>): User { const currentUser = getUserById(id); return { ...currentUser, ...updates };}
// Можем обновлять только нужные поляupdateUser('123', { name: 'Alice' }); // ✓updateUser('123', { name: 'Bob', age: 31 }); // ✓Required<T>
Заголовок раздела «Required<T>»Required<T> делает все свойства типа T обязательными (удаляет ?):
// Определение Required (встроенное)type Required<T> = { [P in keyof T]-?: T[P];};
// Пример использованияinterface Config { host?: string; port?: number; ssl?: boolean;}
// Все поля обязательныtype RequiredConfig = Required<Config>;// {// host: string;// port: number;// ssl: boolean;// }
// Практическое применение: валидация конфигурацииfunction validateConfig(config: Config): config is RequiredConfig { return ( config.host !== undefined && config.port !== undefined && config.ssl !== undefined );}
function startServer(config: Config): void { if (!validateConfig(config)) { throw new Error('Invalid config'); }
// Теперь TypeScript знает, что все поля есть console.log(`Server starting on ${config.host}:${config.port}`);}Readonly<T>
Заголовок раздела «Readonly<T>»Readonly<T> делает все свойства типа T только для чтения:
// Определение Readonly (встроенное)type Readonly<T> = { readonly [P in keyof T]: T[P];};
// Пример использованияinterface Point { x: number; y: number;}
type ReadonlyPoint = Readonly<Point>;// {// readonly x: number;// readonly y: number;// }
const point: ReadonlyPoint = { x: 10, y: 20 };// point.x = 30; // ✗ Ошибка: readonly
// Практическое применение: immutable данныеfunction createImmutableConfig<T extends object>(config: T): Readonly<T> { return Object.freeze(config);}
const appConfig = createImmutableConfig({ apiUrl: 'https://api.example.com', timeout: 5000,});
// appConfig.apiUrl = 'https://new-api.com'; // ✗ Ошибка: readonlyКомбинирование Utility Types
Заголовок раздела «Комбинирование Utility Types»// Partial + Readonly = опциональные readonly поляtype PartialReadonly<T> = Partial<Readonly<T>>;
interface Settings { theme: 'light' | 'dark'; language: string; notifications: boolean;}
type PartialReadonlySettings = PartialReadonly<Settings>;// {// readonly theme?: 'light' | 'dark';// readonly language?: string;// readonly notifications?: boolean;// }
// Required + Readonly = все обязательные и readonlytype RequiredReadonly<T> = Required<Readonly<T>>;
// Conditional utility typetype MakeRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface Form { name?: string; email?: string; age?: number;}
// Делаем только email обязательнымtype FormWithRequiredEmail = MakeRequired<Form, 'email'>;// {// name?: string;// email: string; // обязательно!// age?: number;// }Практический пример: Form State
Заголовок раздела «Практический пример: Form State»// Система управления состоянием формыinterface FormField<T> { value: T; error: string | null; touched: boolean;}
type FormState<T extends Record<string, any>> = { [K in keyof T]: FormField<T[K]>;};
// Initial state - все поля опциональныtype InitialFormState<T extends Record<string, any>> = Partial< FormState<T>>;
// Submitted state - все поля обязательны и readonlytype SubmittedFormState<T extends Record<string, any>> = Readonly< Required<FormState<T>>>;
// Определение формыinterface LoginFormData { email: string; password: string; rememberMe: boolean;}
class FormManager<T extends Record<string, any>> { private state: FormState<T>;
constructor(initialState: InitialFormState<T>) { this.state = {} as FormState<T>;
for (const key in initialState) { this.state[key] = initialState[key]!; } }
setValue<K extends keyof T>(field: K, value: T[K]): void { this.state[field].value = value; this.state[field].touched = true; }
setError<K extends keyof T>(field: K, error: string | null): void { this.state[field].error = error; }
getState(): Readonly<FormState<T>> { return this.state; }
submit(): SubmittedFormState<T> | null { // Валидация - все поля заполнены for (const key in this.state) { if (this.state[key].error !== null) { return null; } }
return Object.freeze(this.state) as SubmittedFormState<T>; }}
// Использованиеconst loginForm = new FormManager<LoginFormData>({ email: { value: '', error: null, touched: false, }, password: { value: '', error: null, touched: false, }, rememberMe: { value: false, error: null, touched: false, },});
loginForm.setValue('password', 'secret123');
const submittedData = loginForm.submit();if (submittedData) { // Все поля гарантированно заполнены и readonly console.log(submittedData.email.value);}Deep Versions
Заголовок раздела «Deep Versions»// Deep Partial - рекурсивно делает все поля optionaltype DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];};
interface NestedConfig { server: { host: string; port: number; ssl: { enabled: boolean; cert: string; }; }; database: { url: string; pool: { min: number; max: number; }; };}
type PartialNestedConfig = DeepPartial<NestedConfig>;// Все вложенные поля опциональны
const partialConfig: PartialNestedConfig = { server: { ssl: { enabled: true, // cert не обязателен }, // host и port не обязательны }, // database не обязателен};
// Deep Readonly - рекурсивно делает все поля readonlytype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];};
type ImmutableConfig = DeepReadonly<NestedConfig>;
const config: ImmutableConfig = { server: { host: 'localhost', port: 3000, ssl: { enabled: true, cert: '/path/to/cert', }, }, database: { url: 'postgresql://localhost', pool: { min: 2, max: 10 }, },};
// config.server.host = 'new'; // ✗ Ошибка: readonly// config.server.ssl.enabled = false; // ✗ Ошибка: readonlyЖизненный пример: API Client
Заголовок раздела «Жизненный пример: API Client»// Type-safe API client с utility types
interface ApiResponse<T> { data: T; status: number; headers: Readonly<Record<string, string>>;}
interface RequestConfig { baseURL?: string; timeout?: number; headers?: Record<string, string>; params?: Record<string, string | number>;}
class ApiClient { private config: Required<Readonly<RequestConfig>>;
constructor(config: RequestConfig = {}) { // Устанавливаем defaults и делаем readonly this.config = Object.freeze({ baseURL: config.baseURL ?? '', timeout: config.timeout ?? 5000, headers: config.headers ?? {}, params: config.params ?? {}, }); }
async get<T>( endpoint: string, config?: Partial<RequestConfig> ): Promise<ApiResponse<T>> { const mergedConfig = { ...this.config, ...config };
// implementation return { data: {} as T, status: 200, headers: {}, }; }
async post<T, D = any>( endpoint: string, data: D, config?: Partial<RequestConfig> ): Promise<ApiResponse<T>> { // implementation return { data: {} as T, status: 201, headers: {}, }; }
// Обновление конфигурации (создаёт новый клиент) withConfig(updates: Partial<RequestConfig>): ApiClient { return new ApiClient({ ...this.config, ...updates, }); }}
// Использованиеconst api = new ApiClient({ baseURL: 'https://api.example.com', timeout: 10000,});
// Partial config для отдельного запросаconst response = await api.get<User>('/user/123', { headers: { 'Authorization': 'Bearer token' },});
// Новый клиент с обновлённой конфигурациейconst apiWithAuth = api.withConfig({ headers: { 'Authorization': 'Bearer token' },});Selective Partial/Required/Readonly
Заголовок раздела «Selective Partial/Required/Readonly»// Делаем только определённые поля optionaltype PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product { id: string; name: string; price: number; description: string;}
// id и name обязательны, остальные опциональныtype ProductInput = PartialBy<Product, 'price' | 'description'>;// {// id: string;// name: string;// price?: number;// description?: string;// }
// Делаем только определённые поля readonlytype ReadonlyBy<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>;
// id readonly, остальные можно менятьtype ProductWithReadonlyId = ReadonlyBy<Product, 'id'>;
// Делаем только определённые поля requiredtype RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>;Ключевые моменты
Заголовок раздела «Ключевые моменты»Partial<T>делает все свойства опциональными (?)Required<T>делает все свойства обязательными (удаляет?)Readonly<T>делает все свойства неизменяемыми (readonly)- Можно комбинировать utility types для создания новых трансформаций
- Deep версии рекурсивно применяют трансформацию ко всем вложенным полям
- Selective версии применяют трансформацию только к выбранным полям
- Используются для форм, API, конфигураций, partial updates
Object.freeze()обеспечивает runtime immutability дляReadonly- Комбинируются с другими utility types (
Pick,Omit) для точного контроля - Критически важны для создания flexible и type-safe API