- Мнемоническая таблица по категориям
- Использование typeof с утилитными типами
- Комбинирование утилитных типов с примерами
- Создание собственных утилитных типов - подробное объяснение
Утилитный тип | Мнемоника | Что делает |
---|---|---|
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 |
Утилитный тип | Мнемоника | Что делает |
---|---|---|
ThisType<T> |
"Маркер контекста" | Маркирует тип this в объектах |
Многие утилитные типы TypeScript обычно используются в сочетании с оператором typeof
. Это не просто стилистический выбор, а необходимость, которая обусловлена типовой системой TypeScript.
// Для функций почти всегда нужен 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> }
С классами ситуация аналогична - почти всегда требуется использовать 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(...)
)
Визуализация:
new User(1, "John") → User { id: 1, name: "John" }
↑ ↑
ConstructorParameters InstanceType
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
с утилитными типами вроде 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
.
// Начальный тип
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
// Функция, которая может вернуть 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);
}
// Класс с конструктором
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 }
);
}
Стандартный 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- Рекурсия позволяет обрабатывать вложенные объекты любой глубины
Создадим тип, противоположный 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 перед использованием
Стандартный 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
для вложенных объектов