Skip to content

Instantly share code, notes, and snippets.

@olegopro
Created May 10, 2025 17:13
Show Gist options
  • Save olegopro/b3819a878d0b5a8e4ca5e80897f5063b to your computer and use it in GitHub Desktop.
Save olegopro/b3819a878d0b5a8e4ca5e80897f5063b to your computer and use it in GitHub Desktop.

Утилитные типы 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(...))

Визуализация:

new User(1, "John") → User { id: 1, name: "John" }
    ↑                     ↑
ConstructorParameters     InstanceType

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:

  1. Две отдельные сферы существования:

    • Значения (функции, переменные, классы как конструкторы) - существуют во время выполнения
    • Типы (интерфейсы, типы) - существуют только во время компиляции
  2. Преобразование между мирами:

    • typeof в контексте типов - преобразует значение в его тип
    • type User = typeof user - получает тип значения user
  3. Почему это важно:

// Функция как значение
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:

  1. [K in keyof T] - перебираем все ключи типа T
  2. readonly - делаем каждое свойство только для чтения
  3. T[K] extends object ? ... : T[K] - проверяем, является ли значение объектом
  4. T[K] extends Function ? T[K] : DeepReadonly<T[K]> - если это функция, оставляем как есть, иначе рекурсивно применяем DeepReadonly
  5. Рекурсия позволяет обрабатывать вложенные объекты любой глубины

Пример 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:

  1. T | null | undefined - объединяет исходный тип с null и undefined
  2. Это простое объединение типов (union type)
  3. Заставляет 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:

  1. [K in keyof T]? - перебираем все ключи T и делаем их необязательными (?)
  2. Проверяем, является ли свойство объектом: T[K] extends object
  3. Специальная обработка для массивов: T[K] extends Array<infer U> с использованием infer для извлечения типа элементов
  4. Специальная обработка для функций: T[K] extends Function ? T[K]
  5. Рекурсивное применение DeepPartial для вложенных объектов
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment