Библиотека conditional-type-checks для TypeScript: тестирование типов
TypeScript — мощный язык со сложной системой типов, включающей условные типы, маппинг, вывод типов и многое другое. Однако разработчики часто сталкиваются с проблемой: как проверить, что типы работают именно так, как задумано? Обычные юнит-тесты проверяют значения во время выполнения, но не могут гарантировать корректность типов на этапе компиляции. Именно здесь на помощь приходит библиотека conditional-type-checks.
Эта библиотека предоставляет набор утилит для статического тестирования типов TypeScript. Она позволяет писать тесты, которые проверяют, что определённый тип соответствует ожиданиям: является ли он подтипом другого, равны ли два типа, можно ли присвоить одно значение другому и так далее. Все проверки выполняются на этапе компиляции, не влияя на рантайм.
Зачем это нужно? Во-первых, для рефакторинга сложных типов — вы можете быть уверены, что изменения не сломают ожидаемое поведение. Во-вторых, для документирования намерений — тесты типов служат живой спецификацией. В-третьих, для библиотек, предоставляющих сложные типы — пользователи будут уверены в их корректности. Библиотека особенно полезна при работе с условными типами (extends, infer), шаблонными литералами и рекурсивными типами.
Установка
Библиотека устанавливается через npm как зависимость для разработки:
npm install --save-dev conditional-type-checks
Или через yarn:
yarn add --dev conditional-type-checks
Никаких дополнительных настроек TypeScript не требуется — библиотека полностью состоит из типов и не генерирует JavaScript-код.
Быстрый старт
Создайте файл type-tests.ts и напишите простейший тест:
import { IsExact } from 'conditional-type-checks';
type TestType = string;
type ExpectedType = string;
// Проверяем, что типы точно совпадают
type TestPass = IsExact<TestType, ExpectedType>; // true
// Если типы не совпадают, будет ошибка компиляции
// type TestFail = IsExact<TestType, number>; // false
Чтобы тест действительно что-то проверял, используйте конструкцию expectType или просто присваивание с проверкой:
import { assert } from 'conditional-type-checks';
type MyType = { name: string; age: number };
// Проверяем, что MyType является объектом с полями name и age
type Check = assert<IsExact<MyType, { name: string; age: number }>>; // OK
Если проверка не проходит, TypeScript выдаст ошибку компиляции с сообщением Type 'false' does not satisfy the constraint 'true'.
Основные методы и функции
Библиотека предоставляет несколько ключевых утилит. Рассмотрим каждую подробно.
1. IsExact<T, U>
Сигнатура: type IsExact<T, U> = ...
Что делает: Проверяет, являются ли типы T и U точно одинаковыми. Возвращает true если типы идентичны, иначе false. Учитывает все нюансы TypeScript, включая readonly, optional и другие модификаторы.
import { IsExact } from 'conditional-type-checks';
type A = { readonly x: number };
type B = { x: number };
type Test1 = IsExact<A, B>; // false — readonly отличается
type Test2 = IsExact<string, string>; // true
type Test3 = IsExact<string | number, number | string>; // true — порядок не важен
type Test4 = IsExact<{ a: string }, { a: string; b?: number }>; // false
2. IsAny<T>
Сигнатура: type IsAny<T> = ...
Что делает: Проверяет, является ли тип T типом any. Это полезно, чтобы убедиться, что вы случайно не получили any из сложного вычисления типов.
import { IsAny } from 'conditional-type-checks';
type Test1 = IsAny<any>; // true
type Test2 = IsAny<unknown>; // false
type Test3 = IsAny<never>; // false
type Test4 = IsAny<string>; // false
3. IsNever<T>
Сигнатура: type IsNever<T> = ...
Что делает: Проверяет, является ли тип T типом never. Полезно для проверки, что условный тип не привёл к пустому множеству.
import { IsNever } from 'conditional-type-checks';
type Test1 = IsNever<never>; // true
type Test2 = IsNever<string>; // false
type Test3 = IsNever<any>; // false
type Test4 = IsNever<string & number>; // true — пересечение string и number даёт never
4. IsUnknown<T>
Сигнатура: type IsUnknown<T> = ...
Что делает: Проверяет, является ли тип T типом unknown. Отличается от IsAny тем, что unknown — это типобезопасная альтернатива any.
import { IsUnknown } from 'conditional-type-checks';
type Test1 = IsUnknown<unknown>; // true
type Test2 = IsUnknown<any>; // false
type Test3 = IsUnknown<string>; // false
5. IsUnion<T>
Сигнатура: type IsUnion<T> = ...
Что делает: Проверяет, является ли тип T объединением (union) нескольких типов. Возвращает true, если тип состоит из двух и более вариантов.
import { IsUnion } from 'conditional-type-checks';
type Test1 = IsUnion<string | number>; // true
type Test2 = IsUnion<string>; // false
type Test3 = IsUnion<never>; // false
type Test4 = IsUnion<any>; // false — any не считается объединением
6. IsIntersection<T>
Сигнатура: type IsIntersection<T> = ...
Что делает: Проверяет, является ли тип T пересечением (intersection) нескольких типов. Возвращает true, если тип состоит из двух и более компонентов, объединённых через &.
import { IsIntersection } from 'conditional-type-checks';
type Test1 = IsIntersection<{ a: string } & { b: number }>; // true
type Test2 = IsIntersection<string>; // false
type Test3 = IsIntersection<string & number>; // true — хотя результат never, это пересечение
7. IsLiteral<T>
Сигнатура: type IsLiteral<T, Expected extends string | number | boolean | bigint | null | undefined> = ...
Что делает: Проверяет, является ли тип T литеральным типом, равным Expected. Полезно для проверки конкретных значений на уровне типов.
import { IsLiteral } from 'conditional-type-checks';
type Test1 = IsLiteral<'hello', 'hello'>; // true
type Test2 = IsLiteral<42, 42>; // true
type Test3 = IsLiteral<string, 'hello'>; // false — string не является литералом
type Test4 = IsLiteral<'hello', 'world'>; // false
8. Has<T, U>
Сигнатура: type Has<T, U> = ...
Что делает: Проверяет, является ли тип T подтипом U (то есть, можно ли присвоить значение типа T переменной типа U). Аналог проверки T extends U, но с более строгой логикой.
import { Has } from 'conditional-type-checks';
type Test1 = Has<string, string | number>; // true — string можно присвоить string | number
type Test2 = Has<string | number, string>; // false — string | number нельзя присвоить string
type Test3 = Has<{ a: string }, { a: string; b: number }>; // false — не хватает b
type Test4 = Has<{ a: string; b: number }, { a: string }>; // true — лишние поля допустимы
9. IsNullable<T>
Сигнатура: type IsNullable<T> = ...
Что делает: Проверяет, может ли тип T принимать значение null или undefined. Возвращает true, если тип включает хотя бы один из этих вариантов.
import { IsNullable } from 'conditional-type-checks';
type Test1 = IsNullable<string | null>; // true
type Test2 = IsNullable<string | undefined>; // true
type Test3 = IsNullable<string>; // false
type Test4 = IsNullable<any>; // false — any не считается nullable
10. assert<T extends true>
Сигнатура: type assert<T extends true> = ...
Что делает: Утилита для явного утверждения, что тип T является true. Если передать false, возникнет ошибка компиляции. Используется для создания читаемых тестов.
import { assert, IsExact } from 'conditional-type-checks';
type MyType = { x: number };
// Утверждаем, что MyType равен ожидаемому типу
type Test1 = assert<IsExact<MyType, { x: number }>>; // OK
// Следующая строка вызовет ошибку компиляции:
// type Test2 = assert<IsExact<MyType, { x: string }>>; // Error!
Реальный практический пример
Рассмотрим реальный сценарий: мы разрабатываем библиотеку для работы с API-ответами, где типы ответов зависят от статуса запроса. Нам нужно убедиться, что все условные типы работают корректно.
// api-types.ts — основные типы библиотеки
import { assert, IsExact, IsNever, IsUnion, Has } from 'conditional-type-checks';
// Определяем возможные статусы ответа
type SuccessStatus = 'success';
type ErrorStatus = 'error';
type LoadingStatus = 'loading';
// Тип для успешного ответа
interface SuccessResponse<T> {
status: SuccessStatus;
data: T;
timestamp: number;
}
// Тип для ответа с ошибкой
interface ErrorResponse {
status: ErrorStatus;
error: string;
code: number;
}
// Тип для состояния загрузки
interface LoadingResponse {
status: LoadingStatus;
progress: number;
}
// Условный тип, который выбирает правильную структуру ответа
type ApiResponse<T, S extends SuccessStatus | ErrorStatus | LoadingStatus> =
S extends SuccessStatus ? SuccessResponse<T> :
S extends ErrorStatus ? ErrorResponse :
S extends LoadingStatus ? LoadingResponse :
never;
// Функция для создания ответа (упрощённо)
function createResponse<T, S extends SuccessStatus | ErrorStatus | LoadingStatus>(
status: S,
payload: T
): ApiResponse<T, S> {
// ... реализация
return {} as any;
}
// ===== ТЕСТЫ ТИПОВ =====
// Тест 1: Проверяем, что ApiResponse для success возвращает правильный тип
type SuccessApi = ApiResponse<{ id: number }, SuccessStatus>;
type ExpectedSuccess = SuccessResponse<{ id: number }>;
type TestSuccess = assert<IsExact<SuccessApi, ExpectedSuccess>>; // OK
// Тест 2: Проверяем, что ApiResponse для error возвращает ErrorResponse
type ErrorApi = ApiResponse<never, ErrorStatus>;
type TestError = assert<IsExact<ErrorApi, ErrorResponse>>; // OK
// Тест 3: Проверяем, что ApiResponse для loading возвращает LoadingResponse
type LoadingApi = ApiResponse<never, LoadingStatus>;
type TestLoading = assert<IsExact<LoadingApi, LoadingResponse>>; // OK
// Тест 4: Проверяем, что never не появляется в неправильных местах
type InvalidStatusApi = ApiResponse<string, 'invalid'>;
type TestInvalid = assert<IsNever<InvalidStatusApi>>; // OK — never для неизвестного статуса
// Тест 5: Проверяем, что SuccessResponse — это объединение с другими? Нет, это отдельный тип
type TestUnion = assert<IsUnion<SuccessResponse<string>>>; // false — не объединение
// Тест 6: Проверяем, что ApiResponse для разных статусов образует объединение
type AllResponses = ApiResponse<string, SuccessStatus> | ApiResponse<string, ErrorStatus> | ApiResponse<string, LoadingStatus>;
type Test Также в библиотеке
validator — Валидация данных в Rust
PDF генерация в Rust: обзор библиотек и инструментов для создания PDF-документов
Генерация PDF в Java: обзор лучших библиотек (iText, Apache PDFBox, OpenPDF)