33. Record, Extract
TypeScript: Броня. Урок 32: Record и Extract
Заголовок раздела «TypeScript: Броня. Урок 32: Record и Extract»Record и Extract - это мощные встроенные utility types для создания объектных типов и извлечения подтипов из union. Они особенно полезны при работе с динамическими ключами, маппингами и discriminated unions.
Record<K, T>
Заголовок раздела «Record<K, T>»Record<K, T> создаёт объектный тип с ключами типа K и значениями типа T:
// Определение Record (встроенное)type Record<K extends keyof any, T> = { [P in K]: T;};
// Простой примерtype UserRoles = Record<string, boolean>;
const roles: UserRoles = { admin: true, moderator: false, user: true,};
// С конкретными ключамиtype HttpStatusCodes = Record<200 | 404 | 500, string>;
const statusMessages: HttpStatusCodes = { 200: 'OK', 404: 'Not Found', 500: 'Internal Server Error',};
// С enum ключамиenum Permission { Read = 'read', Write = 'write', Delete = 'delete',}
type PermissionMap = Record<Permission, boolean>;
const userPermissions: PermissionMap = { [Permission.Read]: true, [Permission.Write]: true, [Permission.Delete]: false,};Record vs Interface
Заголовок раздела «Record vs Interface»// Record - для динамических ключейtype DynamicConfig = Record<string, number | string | boolean>;
const config: DynamicConfig = { port: 3000, host: 'localhost', ssl: true, timeout: 5000,};
// Interface - для известных ключейinterface StaticConfig { port: number; host: string; ssl: boolean;}
const staticConfig: StaticConfig = { port: 3000, host: 'localhost', ssl: true,};
// Когда использовать Record:// - Ключи известны заранее и их немного (union)// - Все значения одного типа или union типов// - Нужна краткость
// Когда использовать interface:// - Ключи известны и их много// - Разные типы для разных ключей// - Нужна расширяемостьПрактический пример: Translations
Заголовок раздела «Практический пример: Translations»// Type-safe система переводовtype Locale = 'en' | 'ru' | 'es' | 'fr';
type TranslationKeys = | 'common.welcome' | 'common.goodbye' | 'errors.notFound' | 'errors.unauthorized' | 'forms.submit' | 'forms.cancel';
type Translations = Record<TranslationKeys, string>;
// Словарь для каждой локалиtype LocaleTranslations = Record<Locale, Translations>;
const translations: LocaleTranslations = { en: { 'common.welcome': 'Welcome', 'common.goodbye': 'Goodbye', 'errors.notFound': 'Not found', 'errors.unauthorized': 'Unauthorized', 'forms.submit': 'Submit', 'forms.cancel': 'Cancel', }, ru: { 'common.welcome': 'Добро пожаловать', 'common.goodbye': 'До свидания', 'errors.notFound': 'Не найдено', 'errors.unauthorized': 'Не авторизован', 'forms.submit': 'Отправить', 'forms.cancel': 'Отмена', }, es: { 'common.welcome': 'Bienvenido', 'common.goodbye': 'Adiós', 'errors.notFound': 'No encontrado', 'errors.unauthorized': 'No autorizado', 'forms.submit': 'Enviar', 'forms.cancel': 'Cancelar', }, fr: { 'common.welcome': 'Bienvenue', 'common.goodbye': 'Au revoir', 'errors.notFound': 'Pas trouvé', 'errors.unauthorized': 'Non autorisé', 'forms.submit': 'Soumettre', 'forms.cancel': 'Annuler', },};
// Type-safe функция переводаfunction t(locale: Locale, key: TranslationKeys): string { return translations[locale][key];}
console.log(t('en', 'common.welcome')); // "Welcome"console.log(t('ru', 'errors.notFound')); // "Не найдено"Extract<T, U>
Заголовок раздела «Extract<T, U>»Extract<T, U> извлекает из union T только те типы, которые присваиваемы к U:
// Определение Extract (встроенное)type Extract<T, U> = T extends U ? T : never;
// С примитивамиtype Mixed = string | number | boolean | null;
type StringOrNumber = Extract<Mixed, string | number>;// string | number
type OnlyString = Extract<Mixed, string>;// string
// С литераламиtype Status = 'pending' | 'approved' | 'rejected' | 'cancelled';
type ActiveStatuses = Extract<Status, 'pending' | 'approved'>;// 'pending' | 'approved'
// С discriminated unionstype Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; size: number } | { kind: 'rectangle'; width: number; height: number };
// Извлекаем только circletype CircleShape = Extract<Shape, { kind: 'circle' }>;// { kind: 'circle'; radius: number }
// Извлекаем shapes с полем 'size'type ShapesWithSize = Extract<Shape, { size: number }>;// { kind: 'square'; size: number }Record + Extract Pattern
Заголовок раздела «Record + Extract Pattern»// Комбинирование Record и Extract для type-safe state management
// Определение всех возможных действийtype Action = | { type: 'USER_LOGIN'; payload: { userId: string; token: string } } | { type: 'USER_LOGOUT' } | { type: 'SET_LOADING'; payload: boolean } | { type: 'SET_ERROR'; payload: string };
// Извлекаем типы действийtype ActionType = Action['type'];// 'USER_LOGIN' | 'USER_LOGOUT' | 'SET_LOADING' | 'SET_ERROR'
// Создаём Record для handlerstype ActionHandlers<S> = Record< ActionType, (state: S, action: Extract<Action, { type: ActionType }>) => S>;
// Но правильнее - для каждого типа свой handlertype ActionHandler<S, T extends ActionType> = ( state: S, action: Extract<Action, { type: T }>) => S;
interface AppState { userId: string | null; token: string | null; loading: boolean; error: string | null;}
// Type-safe handlersconst handlers: { [K in ActionType]: ActionHandler<AppState, K>;} = { USER_LOGIN: (state, action) => ({ ...state, userId: action.payload.userId, token: action.payload.token, }),
USER_LOGOUT: (state, action) => ({ ...state, userId: null, token: null, }),
SET_LOADING: (state, action) => ({ ...state, loading: action.payload, }),
SET_ERROR: (state, action) => ({ ...state, error: action.payload, }),};
// Reducerfunction reducer(state: AppState, action: Action): AppState { const handler = handlers[action.type]; return handler(state, action as any);}Жизненный пример: Router Configuration
Заголовок раздела «Жизненный пример: Router Configuration»// Type-safe роутер с Record и Extracttype RouteParams = Record<string, string | number>;
interface RouteConfig<P extends RouteParams = {}> { path: string; component: string; params: P; meta?: { requiresAuth?: boolean; roles?: string[]; };}
// Определение всех роутовtype AppRoutes = { home: RouteConfig; userProfile: RouteConfig<{ userId: string }>; userPosts: RouteConfig<{ userId: string; page: number }>; postDetail: RouteConfig<{ postId: string }>; settings: RouteConfig;};
// Извлекаем роуты с параметрамиtype RoutesWithParams = { [K in keyof AppRoutes]: AppRoutes[K] extends RouteConfig<infer P> ? P extends {} ? keyof P extends never ? never : K : never : never;}[keyof AppRoutes];// 'userProfile' | 'userPosts' | 'postDetail'
// Конфигурация роутовconst routes: AppRoutes = { home: { path: '/', component: 'HomePage', params: {}, }, userProfile: { path: '/users/:userId', component: 'UserProfilePage', params: { userId: '' }, meta: { requiresAuth: true }, }, userPosts: { path: '/users/:userId/posts', component: 'UserPostsPage', params: { userId: '', page: 1 }, }, postDetail: { path: '/posts/:postId', component: 'PostDetailPage', params: { postId: '' }, }, settings: { path: '/settings', component: 'SettingsPage', params: {}, meta: { requiresAuth: true }, },};
// Type-safe навигацияfunction navigate<K extends keyof AppRoutes>( route: K, params: AppRoutes[K]['params']): void { const config = routes[route]; let path = config.path;
for (const key in params) { path = path.replace(`:${key}`, String(params[key])); }
console.log(`Navigating to: ${path}`);}
// Использованиеnavigate('userProfile', { userId: '123' }); // ✓navigate('userPosts', { userId: '123', page: 2 }); // ✓// navigate('userProfile', { userId: 123 }); // ✗ Ошибка: number не string// navigate('userProfile', {}); // ✗ Ошибка: отсутствует userIdRecord с Nested Types
Заголовок раздела «Record с Nested Types»// Вложенные Record типыtype NestedConfig = Record< string, Record<string, string | number | boolean>>;
const appConfig: NestedConfig = { server: { port: 3000, host: 'localhost', ssl: true, }, database: { url: 'postgresql://localhost', pool: 10, timeout: 5000, },};
// Типизированные вложенные структурыtype ConfigSections = 'server' | 'database' | 'cache';
type ConfigValue = string | number | boolean;
type AppConfig = Record<ConfigSections, Record<string, ConfigValue>>;
const typedConfig: AppConfig = { server: { port: 3000, host: 'localhost', }, database: { url: 'postgresql://localhost', }, cache: { enabled: true, ttl: 3600, },};Partial Record
Заголовок раздела «Partial Record»// Record с опциональными значениямиtype PartialRecord<K extends keyof any, T> = { [P in K]?: T;};
// Кэш - не все ключи обязательныtype Cache = PartialRecord<string, any>;
const cache: Cache = { 'user:123': { name: 'Alice' }, 'post:456': { title: 'Hello' },};
// Можно добавлять новые ключиcache['comment:789'] = { text: 'Great!' };
// Можно не заполнять все возможные ключиtype Features = 'darkMode' | 'notifications' | 'analytics';
type FeatureFlags = PartialRecord<Features, boolean>;
const flags: FeatureFlags = { darkMode: true, // notifications и analytics опциональны};Advanced Extract Patterns
Заголовок раздела «Advanced Extract Patterns»// Извлечение функциональных типовtype MixedTypes = string | number | (() => void) | ((x: number) => string);
type OnlyFunctions = Extract<MixedTypes, Function>;// (() => void) | ((x: number) => string)
// Извлечение async функцийtype AsyncFunctions = Extract<MixedTypes, () => Promise<any>>;
// Извлечение по структуреtype Event = | { type: 'click'; x: number; y: number } | { type: 'keypress'; key: string } | { type: 'scroll'; delta: number } | { type: 'resize'; width: number; height: number };
// События с координатамиtype EventsWithCoordinates = Extract<Event, { x: number }>;// { type: 'click'; x: number; y: number }
// События с type начинающимся на 's'type EventsStartingWithS = Extract<Event, { type: `s${string}` }>;// { type: 'scroll'; delta: number }Ключевые моменты
Заголовок раздела «Ключевые моменты»Record<K, T>создаёт объектный тип с ключамиKи значениямиT- Удобен для маппингов, словарей, конфигураций
Extract<T, U>извлекает из union типы, присваиваемые кU- Противоположность
Exclude - Комбинируются для создания type-safe state management
- Record идеален для enum-to-value маппингов
- Extract полезен для работы с discriminated unions
- Можно создавать вложенные Record типы
- PartialRecord для опциональных ключей
- Широко используются в роутинге, переводах, конфигурациях