Утилитные типы TypeScript — расширенное руководство

Содержание
Мнемоническая таблица по категориям
Объектные утилитные типы
Изменяют структуру объекта
Утилитный тип | Мнемоника | Что делает |
---|---|---|
Partial<T> | "Частичный" | Делает все свойства необязательными |
Required<T> | "Обязательный" | Делает все свойства обязательными |
Pick<T, K> | "Выбрать" | Выбирает указанные свойства |
Omit<T, K> | "Опустить" | Исключает указанные свойства |
Record<K, T> | "Запись в БД" | Создает {} с ключами K и значениями T |
Readonly<T> | "Только чтение" | Делает все свойства только для чтения |
Функциональные утилитные типы
Работают с функциями
Утилитный тип | Мнемоника | Что делает |
---|---|---|
ReturnType<T> | "Тип выхода" | Извлекает тип возвращаемого значения |
Parameters<T> | "Тип входа" | Извлекает типы параметров функции |
OmitThisParameter<T> | "Без контекста" | Удаляет this-параметр |
ThisParameterType<T> | "Тип контекста" | Извлекает тип this-параметра |
Утилитные типы для работы с классами
Утилитный тип | Мнемоника | Что делает |
---|---|---|
InstanceType<T> | "Что возвращает new" | Тип экземпляра класса |
ConstructorParameters<T> | "Что принимает new" | Типы параметров конструктора |
Утилитные типы для фильтрации типов
Утилитный тип | Мнемоника | Что делает |
---|---|---|
NonNullable<T> | "Не нулевой" | Удаляет null и undefined из типа |
Exclude<T, U> | "Исключить" | Исключает типы из T , присутствующие в U |
Extract<T, U> | "Извлечь" | Извлекает из T типы, присутствующие в U |
Утилитные типы для контекста this
Утилитный тип | Мнемоника | Что делает |
---|---|---|
ThisType<T> | "Маркер контекста" | Маркирует тип this в объектах |
Использование typeof с утилитными типами
Многие утилитные типы TypeScript обычно используются в сочетании с оператором typeof
. Это не просто стилистический выбор, а необходимость, которая обусловлена типовой системой TypeScript.
ReturnType и Parameters
// Для функций почти всегда нужен typeof function createUser() { return { id: 1, name: "John" }; } // Правильно - с typeof type User = ReturnType<typeof createUser>; // { id: number; name: string; } // Неправильно - без typeof // type User = ReturnType<createUser>; // Ошибка: 'createUser' refers to a value, but is being used as a type here
При использовании Parameters
результат возвращается в виде кортежа (tuple):
function fetchData(url: string, options: { method: string; headers?: Record<string, string> }) { // реализация } type FetchParams = Parameters<typeof fetchData>; // [url: string, options: { method: string; headers?: Record<string, string> }] // Можно обращаться к параметрам по индексу type OptionsType = FetchParams[1]; // { method: string; headers?: Record<string, string> }
InstanceType и ConstructorParameters
С классами ситуация аналогична - почти всегда требуется использовать typeof
:
class User { constructor(public id: number, public name: string) {} greet() { return `Hello, ${this.name}`; } } // Правильно - с typeof type UserInstance = InstanceType<typeof User>; // { id: number; name: string; greet(): string; } // Неправильно - ошибка компиляции // type UserInstance = InstanceType<User>; // 'User' refers to a value, but is being used as a type // То же самое с ConstructorParameters type UserConstructorParams = ConstructorParameters<typeof User>; // [id: number, name: string]
Важно понимать разницу между InstanceType
и ConstructorParameters
:
InstanceType<typeof Class>
- тип экземпляра класса (тип объекта, создаваемого черезnew Class()
)ConstructorParameters<typeof Class>
- типы параметров конструктора (параметры, передаваемые вnew Class(...)
)
Более точное объяснение:
ConstructorParameters<typeof User>
=[number, string]
- это tuple типов параметров конструктораInstanceType<typeof User>
=User
- это тип экземпляра класса
ThisParameterType
ThisParameterType
извлекает тип this
-параметра функции, когда он явно указан:
// Функция с явным this-параметром function greet(this: { name: string }, prefix: string) { return `${prefix}, ${this.name}!`; } // Получаем тип this-параметра type GreetThisType = ThisParameterType<typeof greet>; // { name: string } // Использование const user = { name: "Alice", age: 30 }; greet.call(user, "Привет"); // OK const invalid = { age: 25 }; // greet.call(invalid, "Привет"); // Ошибка: объект не имеет свойства 'name'
Причины использования typeof
Необходимость использования typeof
с утилитными типами вроде ReturnType
, Parameters
, InstanceType
связана с фундаментальным различием между значениями и типами в TypeScript:
- Две отдельные сферы существования:
- Значения (функции, переменные, классы как конструкторы) - существуют во время выполнения
- Типы (интерфейсы, типы) - существуют только во время компиляции
- Преобразование между мирами:
typeof
в контексте типов - преобразует значение в его типtype User = typeof user
- получает тип значенияuser
- Почему это важно:
// Функция как значение function process() { return 42; } // Интерфейс как тип interface Config { debug: boolean; } // Утилитные типы работают с ТИПАМИ, а не ЗНАЧЕНИЯМИ type ProcessReturnType = ReturnType<typeof process>; // OK: number // type InvalidType = ReturnType<process>; // Ошибка: process - это значение, а не тип type ConfigKeys = keyof Config; // OK: "debug" // type InvalidKeys = keyof process; // Ошибка: process - это значение, а не тип
Эта концепция разделения значений и типов - одна из ключевых особенностей TypeScript. Она позволяет системе типов быть выразительной, не влияя на выполнение программы.
Запомните правило: если вы работаете с функцией, классом или другим значением в контексте типов, почти всегда нужно использовать
typeof
Комбинирование утилитных типов с примерами
Пример 1: ReadonlyUserBasicInfo
// Начальный тип interface User { id: number; name: string; email: string; password: string; lastLogin: Date; } // Комбинирование: Pick + Readonly type ReadonlyUserBasicInfo = Readonly<Pick<User, 'id' | 'name'>>; // Эквивалентно: // { // readonly id: number; // readonly name: string; // } const userInfo: ReadonlyUserBasicInfo = { id: 1, name: "Иван" }; // userInfo.name = "Петр"; // Ошибка: Cannot assign to 'name' because it is a read-only property
Пример 2: SafeApiResult
// Функция, которая может вернуть null function fetchData(): { data: string[] } | null { // ... return Math.random() > 0.5 ? { data: ["abc"] } : null; } // Комбинирование: ReturnType + NonNullable type SafeApiResult = NonNullable<ReturnType<typeof fetchData>>; // Эквивалентно: // { // data: string[]; // } // Использование: function processSafeData(data: SafeApiResult) { // Здесь не нужна проверка на null console.log(data.data.length); }
Пример 3: OptionalUserParams
// Класс с конструктором class UserManager { constructor( userId: number, options: { includeDetails: boolean fetchRoles: boolean cacheTTL: number } ) { // ... } } // Комбинирование: ConstructorParameters + Partial type UserManagerParams = ConstructorParameters<typeof UserManager> type OptionalUserManagerOptions = Partial<UserManagerParams[1]> // Эквивалентно: // { // includeDetails?: boolean; // fetchRoles?: boolean; // cacheTTL?: number; // } // Функция с опциональными параметрами function createUserManager(userId: number, options?: OptionalUserManagerOptions) { const defaultOptions = { includeDetails: false, fetchRoles: false, cacheTTL: 3600 } return new UserManager( userId, { ...defaultOptions, ...options } ) }
Создание собственных утилитных типов
Пример 1: DeepReadonly
Стандартный Readonly<T>
делает только "поверхностную" заморозку объекта - вложенные объекты остаются изменяемыми:
interface User { id: number name: string settings: { theme: string notifications: boolean } } type ReadonlyUser = Readonly<User> const user: ReadonlyUser = { id: 1, name: "Alice", settings: { theme: "dark", notifications: true } } // Ошибка - нельзя изменить свойство верхнего уровня // user.name = "Bob"; // Но вложенный объект можно изменить! user.settings.theme = "light" // Работает без ошибок
Создадим рекурсивный тип DeepReadonly
, который замораживает все уровни вложенности:
type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? T[K] extends Function ? T[K] : DeepReadonly<T[K]> : T[K] } // Использование type DeepReadonlyUser = DeepReadonly<User> const deepUser: DeepReadonlyUser = { id: 1, name: 'Alice', settings: { theme: 'dark', notifications: true }, } // Ошибка - нельзя изменить свойство верхнего уровня // deepUser.name = "Bob"; // Теперь и вложенный объект нельзя изменить! // deepUser.settings.theme = "light"; // Ошибка: Cannot assign to 'theme' because it is a read-only property
Разбор типа DeepReadonly:
[K in keyof T]
- перебираем все ключи типа Treadonly
- делаем каждое свойство только для чтенияT[K] extends object ? ... : T[K]
- проверяем, является ли значение объектомT[K] extends Function ? T[K] : DeepReadonly<T[K]>
- если это функция, оставляем как есть, иначе рекурсивно применяем DeepReadonly- Рекурсия позволяет обрабатывать вложенные объекты любой глубины
Пример 2: Nullable
Создадим тип, противоположный NonNullable
- делающий тип допускающим null
и undefined
:
type Nullable<T> = T | null | undefined // Использование function fetchUser(id: number): Nullable<User> { // Может вернуть пользователя, null или undefined if (id < 0) return null if (id === 0) return undefined return { id, name: 'User ' + id } } // Теперь нужно проверять результат перед использованием const user = fetchUser(5) if (user) { console.log(user.name) // Безопасно, только если user не null/undefined }
Разбор типа Nullable:
T | null | undefined
- объединяет исходный тип с null и undefined- Это простое объединение типов (union type)
- Заставляет TypeScript требовать проверки на null/undefined перед использованием
Пример 3: DeepPartial
Стандартный Partial<T>
делает необязательными только свойства верхнего уровня. Создадим рекурсивную версию:
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? T[K] extends Array<infer U> ? Array<DeepPartial<U>> : T[K] extends Function ? T[K] : DeepPartial<T[K]> : T[K] } // Исходный сложный тип interface Config { server: { host: string port: number ssl: { enabled: boolean cert: string key: string } } database: { url: string credentials: { username: string password: string } } features: string[] } // Использование DeepPartial для частичного обновления function updateConfig(config: Config, updates: DeepPartial<Config>): Config { // Реализация слияния объектов return deepMerge(config, updates) } // Теперь можно передать только нужные поля на любом уровне вложенности updateConfig(currentConfig, { server: { ssl: { enabled: true, // Не нужно указывать cert и key }, }, // Не нужно указывать database и features })
Разбор типа DeepPartial:
[K in keyof T]?
- перебираем все ключи T и делаем их необязательными (?)- Проверяем, является ли свойство объектом:
T[K] extends object
- Специальная обработка для массивов:
T[K] extends Array<infer U>
с использованиемinfer
для извлечения типа элементов - Специальная обработка для функций:
T[K] extends Function ? T[K]
- Рекурсивное применение
DeepPartial
для вложенных объектов
Оригинальный текст статьи gist.github.com/olegopro