Что такое Type-fest и зачем он нужен?
Type-fest — это коллекция утилитарных типов для TypeScript, созданная сообществом Sindre Sorhus. Эта библиотека предоставляет десятки готовых типов, которые решают повседневные задачи при разработке на TypeScript: от простых преобразований (например, сделать все поля объекта необязательными) до сложных условных типов (например, извлечь тип значения из Promise).
Зачем нужна эта библиотека? TypeScript — мощный язык, но его встроенные типы (Partial, Required, Pick, Record) покрывают лишь базовые сценарии. В реальных проектах часто требуются более специфичные конструкции: глубокое объединение объектов, типы для работы с JSON, строгая типизация констант, извлечение типов из сложных структур и т.д. Type-fest избавляет от необходимости писать эти типы вручную, экономя время и снижая вероятность ошибок.
Библиотека активно поддерживается, имеет более 10 000 звезд на GitHub и используется в таких крупных проектах, как Next.js, Vite и Expo.
Установка
Type-fest — это npm-пакет, который устанавливается как dev-зависимость (так как типы не влияют на рантайм).
# npm
npm install --save-dev type-fest
# yarn
yarn add --dev type-fest
# pnpm
pnpm add --save-dev type-fest
Обратите внимание: библиотека не устанавливается через pip — она предназначена исключительно для TypeScript/JavaScript.
Быстрый старт
После установки вы можете импортировать любой тип из библиотеки. Рассмотрим простейший пример — использование типа PartialDeep, который делает все поля объекта (включая вложенные) необязательными.
import type { PartialDeep } from 'type-fest';
interface User {
id: number;
name: string;
address: {
city: string;
zip: number;
};
}
// Обычный Partial делает необязательными только поля первого уровня
type PartialUser = Partial<User>;
// { id?: number; name?: string; address?: { city: string; zip: number; } }
// PartialDeep делает необязательными все вложенные поля
type DeepPartialUser = PartialDeep<User>;
// { id?: number; name?: string; address?: { city?: string; zip?: number; } }
const user: DeepPartialUser = {
name: 'Alice',
address: {
city: 'Wonderland'
// zip можно не указывать
}
};
Как видите, PartialDeep автоматически проходится по всем уровням вложенности — это то, что приходится писать вручную с помощью рекурсивных типов.
Основные методы/типы
Рассмотрим 10 наиболее полезных типов из Type-fest. Для каждого приведено описание, параметры (если есть) и рабочий пример.
1. PartialDeep
Что делает: Делает все свойства объекта (включая вложенные) необязательными. Рекурсивно обходит все уровни.
Параметры: Один обязательный — тип объекта.
import type { PartialDeep } from 'type-fest';
interface Config {
server: {
host: string;
port: number;
};
debug: boolean;
}
type PartialConfig = PartialDeep<Config>;
// {
// server?: {
// host?: string;
// port?: number;
// };
// debug?: boolean;
// }
const config: PartialConfig = {
server: {
host: 'localhost'
// port не указан — ошибки нет
}
};
2. RequiredDeep
Что делает: Делает все свойства объекта (включая вложенные) обязательными. Противоположность PartialDeep.
Параметры: Тип объекта.
import type { RequiredDeep } from 'type-fest';
interface User {
name?: string;
address?: {
city?: string;
};
}
type StrictUser = RequiredDeep<User>;
// {
// name: string;
// address: {
// city: string;
// };
// }
// Теперь все поля обязательны
const user: StrictUser = {
name: 'Bob',
address: {
city: 'NYC'
}
};
3. ReadonlyDeep
Что делает: Делает все свойства объекта (включая вложенные) readonly. Рекурсивно.
Параметры: Тип объекта.
import type { ReadonlyDeep } from 'type-fest';
interface Data {
value: number;
nested: {
items: string[];
};
}
type ReadonlyData = ReadonlyDeep<Data>;
const data: ReadonlyData = {
value: 42,
nested: {
items: ['a', 'b']
}
};
// data.value = 100; // Ошибка: нельзя изменить readonly поле
// data.nested.items.push('c'); // Ошибка: метод push недоступен для readonly массива
4. PickDeep
Что делает: Выбирает указанные ключи из объекта, включая вложенные (с использованием точечной нотации).
Параметры: Тип объекта и список ключей (строками через точку для вложенных).
import type { PickDeep } from 'type-fest';
interface User {
id: number;
name: string;
address: {
city: string;
zip: number;
};
}
type UserCity = PickDeep<User, 'id' | 'address.city'>;
// {
// id: number;
// address: {
// city: string;
// };
// }
const user: UserCity = {
id: 1,
address: {
city: 'London'
}
};
5. OmitDeep
Что делает: Исключает указанные ключи из объекта, включая вложенные. Противоположность PickDeep.
Параметры: Тип объекта и список ключей для исключения.
import type { OmitDeep } from 'type-fest';
interface Product {
id: number;
name: string;
price: number;
metadata: {
createdAt: Date;
updatedAt: Date;
};
}
type ProductPublic = OmitDeep<Product, 'metadata.createdAt' | 'metadata.updatedAt'>;
// {
// id: number;
// name: string;
// price: number;
// metadata: {};
// }
6. Merge
Что делает: Объединяет два типа, при этом свойства второго типа переопределяют свойства первого (поверхностное слияние).
Параметры: Два типа — базовый и переопределяющий.
import type { Merge } from 'type-fest';
interface Base {
id: number;
name: string;
}
interface Override {
name: number; // меняем тип name
extra: boolean;
}
type Merged = Merge<Base, Override>;
// {
// id: number;
// name: number; // тип переопределен
// extra: boolean;
// }
7. MergeDeep
Что делает: Глубокое слияние двух типов. Рекурсивно объединяет вложенные объекты, а не заменяет их целиком.
Параметры: Два типа.
import type { MergeDeep } from 'type-fest';
interface Base {
a: {
b: number;
c: string;
};
}
interface Override {
a: {
b: string; // меняем тип
d: boolean; // добавляем новое поле
};
}
type Merged = MergeDeep<Base, Override>;
// {
// a: {
// b: string; // переопределено
// c: string; // сохранено из Base
// d: boolean; // добавлено из Override
// };
// }
8. Simplify
Что делает: «Упрощает» сложные пересекающиеся типы, превращая их в плоскую структуру. Полезно для улучшения читаемости в IDE.
Параметры: Тип.
import type { Simplify } from 'type-fest';
type ComplexType = { a: number } & { b: string } & { c: boolean };
// В IDE будет показано как пересечение
type SimpleType = Simplify<ComplexType>;
// { a: number; b: string; c: boolean; } — плоская структура
9. JsonValue, JsonObject, JsonArray
Что делает: Набор типов для строгой типизации JSON-данных. JsonValue — это объединение всех допустимых JSON-типов, JsonObject — объект с ключами-строками и значениями JsonValue, JsonArray — массив JsonValue.
Параметры: Нет (это конкретные типы).
import type { JsonValue, JsonObject, JsonArray } from 'type-fest';
const data: JsonObject = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'NYC'
}
};
// Ошибка: undefined не является JsonValue
// const invalid: JsonObject = { key: undefined };
function processJson(input: JsonValue): void {
if (typeof input === 'string') {
console.log(input.toUpperCase());
} else if (Array.isArray(input)) {
input.forEach(processJson);
}
}
10. SetNonNullable
Что делает: Исключает null и undefined из типа. Аналог NonNullable из стандартной библиотеки, но работает с объектами.
Параметры: Тип.
import type { SetNonNullable } from 'type-fest';
interface User {
id: number | null;
name: string | undefined;
email: string;
}
type CleanUser = SetNonNullable<User>;
// {
// id: number;
// name: string;
// email: string;
// }
Реальный практический пример использования
Представим, что мы разрабатываем API-клиент для работы с пользователями. Нам нужно:
- Сделать все поля входных данных необязательными (частичное обновление)
- Гарантировать, что ответ от сервера содержит все поля
- Защитить данные от случайных изменений (readonly)
- Парсить JSON-ответ с правильной типизацией
import type { PartialDeep, RequiredDeep, ReadonlyDeep, JsonObject } from 'type-fest';
// Исходная модель пользователя
interface User {
id: number;
name: string;
email: string;
settings: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
// Тип для частичного обновления (PATCH-запрос)
type UpdateUserPayload = PartialDeep<User>;
// Тип для ответа от сервера (все поля обязательны)
type UserResponse = RequiredDeep<User>;
// Тип для отображения (только для чтения)
type UserDisplay = ReadonlyDeep<User>;
class UserApiClient {
async updateUser(id: number, payload: UpdateUserPayload): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(payload)
});
const data: JsonObject = await response.json();
return data as unknown as UserResponse;
}
async getUser(id: number): Promise<UserDisplay> {
const response = await fetch(`/api/users/${id}`);
const data: JsonObject = await response.json();
return data as unknown as UserDisplay;
}
}
// Использование
const client = new UserApiClient();
// Частичное обновление — передаем только изменяемые поля
client.updateUser(1, {
settings: {
theme: 'dark'
// notifications не указано — ошибки нет
}
}).then(user => {
console.log(user.name); // ОК
// user.name = 'New'; // Ошибка: readonly
});
Этот пример демонстрирует, как Type-fest помогает создавать строгие, но гибкие типы для работы с API, избегая дублирования кода.