conditional-type-checks для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Библиотека 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                    

Рекомендаций