27. Дефолтные значения Generic
TypeScript: Броня. Урок 26: Generic Default Parameters
Заголовок раздела «TypeScript: Броня. Урок 26: Generic Default Parameters»Generic default parameters позволяют указать значение по умолчанию для generic параметра типа. Если тип не указан явно и не может быть выведен из контекста, будет использовано значение по умолчанию. Это делает generic типы более удобными в использовании, сохраняя их гибкость.
Базовый синтаксис
Заголовок раздела «Базовый синтаксис»// Generic с default значениемinterface Box<T = string> { value: T;}
// Использование без указания типа - используется default (string)const stringBox: Box = { value: 'hello',};
// Явное указание типаconst numberBox: Box<number> = { value: 42,};
// Type inference работаетconst inferredBox: Box = { value: 'inferred', // T выводится как string};
// Функция с default genericfunction createArray<T = number>(length: number, value: T): T[] { return Array(length).fill(value);}
const numbers = createArray(3, 42); // T = numberconst strings = createArray(3, 'hello'); // T = stringconst defaults = createArray<>(5, 10); // T = number (default)Множественные Default Параметры
Заголовок раздела «Множественные Default Параметры»// Несколько generic параметров с defaultsinterface Response<T = any, E = Error> { data?: T; error?: E; loading: boolean;}
// Все defaultsconst response1: Response = { loading: false, data: { message: 'success' }, // any};
// Первый указан, второй defaultconst response2: Response<User> = { loading: false, data: { id: '1', name: 'Alice' }, // User error: new Error('Failed'), // Error};
// Оба указаныconst response3: Response<Post, string> = { loading: false, error: 'Not found', // string};
// Функция с множественными defaultsfunction fetch<T = any, E = string>( url: string): Promise<Response<T, E>> { return fetch(url) .then(res => res.json()) .then(data => ({ data, loading: false })) .catch(error => ({ error: error.message, loading: false }));}Default Зависящий от Другого Параметра
Заголовок раздела «Default Зависящий от Другого Параметра»// Default может ссылаться на предыдущие параметрыtype Container<T, U = T[]> = { single: T; multiple: U;};
const numbers: Container<number> = { single: 42, multiple: [1, 2, 3], // T[] (number[])};
const mixed: Container<number, string[]> = { single: 42, multiple: ['a', 'b', 'c'], // string[]};
// Более сложный примерtype Result<T, E = Error, M = { data: T; error: E }> = { value: T; error: E; metadata: M;};
// Все defaults применяютсяconst result1: Result<string> = { value: 'success', error: new Error(), metadata: { data: 'success', error: new Error(), },};Практический пример: API Client
Заголовок раздела «Практический пример: API Client»// Type-safe API client с разумными defaultsinterface ApiResponse<T = any> { data: T; status: number; headers: Record<string, string>;}
interface ApiError<T = string> { message: T; code: number; details?: any;}
interface ApiConfig< TResponse = any, TError = string, THeaders = Record<string, string>> { baseUrl?: string; headers?: THeaders; timeout?: number; onSuccess?: (response: TResponse) => void; onError?: (error: ApiError<TError>) => void;}
class ApiClient< TResponse = any, TError = string, THeaders = Record<string, string>> { constructor(private config: ApiConfig<TResponse, TError, THeaders> = {}) {}
async get<T = TResponse>( endpoint: string ): Promise<ApiResponse<T>> { // implementation return {} as ApiResponse<T>; }
async post<T = TResponse, D = any>( endpoint: string, data: D ): Promise<ApiResponse<T>> { // implementation return {} as ApiResponse<T>; }}
// Использование с defaultsconst api = new ApiClient(); // все defaults (any, string, Record<string, string>)
// С частичной типизациейinterface User { id: string; name: string;}
const userApi = new ApiClient<User>(); // TResponse = User, остальные defaults
// С полной типизациейinterface CustomError { message: string; code: string; stack: string;}
const typedApi = new ApiClient<User, CustomError>({ onError: (err) => { console.log(err.code); // string (CustomError.code) },});Default Constraints
Заголовок раздела «Default Constraints»// Default с ограничениямиinterface Entity { id: string;}
interface DefaultEntity extends Entity { createdAt: Date;}
// Default должен соответствовать constrainttype Repository<T extends Entity = DefaultEntity> = { findById(id: string): Promise<T>; findAll(): Promise<T[]>; create(entity: Omit<T, 'id'>): Promise<T>;};
// Использование с defaultconst defaultRepo: Repository = { async findById(id) { return { id, createdAt: new Date(), }; }, async findAll() { return []; }, async create(entity) { return { ...entity, id: Math.random().toString(36), }; },};
// С кастомным типомinterface User extends Entity { name: string; email: string;}
const userRepo: Repository<User> = { async findById(id) { return { id, name: 'Alice', }; }, async findAll() { return []; }, async create(entity) { return { ...entity, id: Math.random().toString(36), }; },};Жизненный пример: State Management
Заголовок раздела «Жизненный пример: State Management»// Redux-like state management с defaults
// Default state shapeinterface DefaultState { loading: boolean; error: string | null;}
// Default actioninterface DefaultAction { type: string; payload?: any;}
// Reducer с default typestype Reducer< S = DefaultState, A extends DefaultAction = DefaultAction> = (state: S, action: A) => S;
// Middleware с defaultstype Middleware< S = DefaultState, A extends DefaultAction = DefaultAction, D = unknown> = (store: Store<S, A, D>) => (next: Dispatch<A>) => Dispatch<A>;
// Store interfaceinterface Store< S = DefaultState, A extends DefaultAction = DefaultAction, D = unknown> { getState(): S; dispatch: Dispatch<A>; subscribe(listener: () => void): () => void;}
type Dispatch<A extends DefaultAction = DefaultAction> = (action: A) => void;
// Создание store с defaultsfunction createStore< S = DefaultState, A extends DefaultAction = DefaultAction>( reducer: Reducer<S, A>, initialState: S): Store<S, A> { let state = initialState; const listeners: Array<() => void> = [];
return { getState: () => state, dispatch: (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }, subscribe: (listener) => { listeners.push(listener); return () => { const index = listeners.indexOf(listener); if (index > -1) listeners.splice(index, 1); }; }, };}
// Использование с defaultsconst defaultReducer: Reducer = (state, action) => { switch (action.type) { case 'SET_LOADING': return { ...state, loading: action.payload }; case 'SET_ERROR': return { ...state, error: action.payload }; default: return state; }};
const store = createStore(defaultReducer, { loading: false, error: null,});
// Типизированное использованиеinterface UserState { users: User[]; loading: boolean; error: string | null;}
interface UserAction { type: 'LOAD_USERS' | 'ADD_USER' | 'DELETE_USER'; payload: any;}
const userReducer: Reducer<UserState, UserAction> = (state, action) => { switch (action.type) { case 'LOAD_USERS': return { ...state, users: action.payload, loading: false }; case 'ADD_USER': return { ...state, users: [...state.users, action.payload] }; case 'DELETE_USER': return { ...state, users: state.users.filter(u => u.id !== action.payload), }; default: return state; }};
const userStore = createStore(userReducer, { users: [], loading: false, error: null,});Conditional Defaults
Заголовок раздела «Conditional Defaults»// Default зависящий от условияtype DefaultValue<T> = T extends string ? '' : T extends number ? 0 : T extends boolean ? false : never;
interface Field<T, D = DefaultValue<T>> { value: T; defaultValue: D;}
const stringField: Field<string> = { value: 'hello', defaultValue: '', // DefaultValue<string> = ''};
const numberField: Field<number> = { value: 42, defaultValue: 0, // DefaultValue<number> = 0};
// Более сложный conditional defaulttype AsyncValue<T, Async extends boolean = false> = Async extends true ? Promise<T> : T;
function getValue<T, A extends boolean = false>( value: T, async?: A): AsyncValue<T, A> { if (async) { return Promise.resolve(value) as any; } return value as any;}
const sync = getValue('hello'); // stringconst async = getValue('hello', true); // Promise<string>Partial Application Pattern
Заголовок раздела «Partial Application Pattern»// Частичное применение generic параметров через defaultstype PartiallyApplied< T, U = any, V = any> = { first: T; second: U; third: V;};
// Создание специализированных версийtype StringFirst<U = string, V = number> = PartiallyApplied<string, U, V>;
type NumberSecond<T = string, V = boolean> = PartiallyApplied<T, number, V>;
// Использованиеconst a: StringFirst = { first: 'text', second: 'default', third: 42,};
const b: NumberSecond = { first: 'text', second: 123, third: true,};
// Применение в функцияхfunction create< T = string, U = number>(first: T, second: U): PartiallyApplied<T, U, boolean> { return { first, second, third: true, };}
const result1 = create('hello', 42); // PartiallyApplied<string, number, boolean>const result2 = create<number>(100, 200); // PartiallyApplied<number, number, boolean>Factory Pattern с Defaults
Заголовок раздела «Factory Pattern с Defaults»// Фабрика с разумными defaultsinterface FactoryOptions<T = any> { validate?: (value: T) => boolean; transform?: (value: T) => T; defaultValue?: T;}
class Factory<T = any> { constructor(private options: FactoryOptions<T> = {}) {}
create(value: T): T { if (this.options.validate && !this.options.validate(value)) { throw new Error('Validation failed'); }
return this.options.transform ? this.options.transform(value) : value; }
createDefault(): T { if (!this.options.defaultValue) { throw new Error('No default value provided'); } return this.options.defaultValue; }}
// Использование с defaultsconst genericFactory = new Factory(); // Factory<any>
// Типизированная фабрикаconst userFactory = new Factory<User>({ validate: (user) => user.name.length > 0,});
const user = userFactory.create({ id: '1', name: 'Alice',});Ключевые моменты
Заголовок раздела «Ключевые моменты»- Default generic параметры указываются через
<T = DefaultType> - Используются когда тип не указан явно и не может быть выведен
- Можно указать defaults для всех или некоторых параметров
- Default может ссылаться на предыдущие generic параметры
- Default должен соответствовать constraint (если есть)
- Полезны для API, которые часто используются с одними типами
- Делают generic типы более удобными без потери гибкости
- Могут быть conditional (зависеть от условий)
- Используются в state management, factories, API clients
- Позволяют создавать partially applied types для специализации