Если даже TypeScript и задумывался просто как «надмножество с типами» над нашим родным JS, то с развитием стал чем-то большим. В арсенале разработчика оказались многие инструменты, позволяющие типам и адаптироваться под логику приложения. Сегодня поговорим про два мощнейших приема, которыми стоит овладеть: Conditional Types (условные типы) и Mapped Types (хорошего перевода не встречал, но самое адекватное и близкое по смыслу — отображенные типы).
1. Conditional Types — if для типов
Условные типы позволяют выбирать тип на основе проверки. Синтаксис похож на тернарный оператор в JS:
type Conditional<T> = T extends U ? X : Y
Если T можно присвоить типу U, то результирующий тип Conditional — X, иначе — Y. Естественно, U, X, Y нужно заменить на какие-то реальные типы. Разберем простой пример и станет понятно.
Представим функцию, которая возвращает длину массива или строки. Мы хотим, чтобы для строки и массива мы возвращали число, а для любых других данных — ошибку. Тогда напишем так.
type LengthOrNever<T> = T extends string | any[] ? number : never;
function getLength<T>(value: T): LengthOrNever<T> {
if (typeof value === "string") return value.length as any;
if (Array.isArray(value)) return value.length as any;
throw new Error("Не поддерживается");
}
const len1 = getLength("hello"); // number
const len2 = getLength([1, 2, 3]); // number
const len3 = getLength(123); // never – такой вызов TS подсветит как ошибку
Чуть менее высосанный из пальца пример — фильтрация юнион-типа. Например, какие-то бэкендеры и фронтендеры, которые работали на проекте до нас, описали что с бэка помимо валидных значений в типе товара может прийти что угодно. Но где-то в коде нам нужно взять только осмысленные значения, отсеяв весь шлак. Условные типы тут как раз к месту.
type ProductTypesAPI = "a" | "b" | undefined | 0 | boolean | null;
type OnlyValidValues<T> = T extends string ? T : never;
type ProductTypes = OnlyValidValues<ProductTypesAPI>; // "a" | "b"
2. Mapped Types — трансформация свойств
Mapped Types позволяют пройти по всем ключам существующего типа и создать новый. Синтаксис напоминает Array.map() для типов.
Если вы используете встроенные ютилити-тайпы, то значит и с маппингом уже сталкивались. Ведь, например, те же Readonly<T> и Partial<T> окажутся именно отраженными типами, если заглянуть в исходный код.
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Но чтобы понять как оно работает на самом деле, нужно написать что-то свое. Представим, что у нас теперь задача обратная тому, что мы делали в последнем примере. У нас на фронте есть прекрасный, чистенький интерфейс с описанием типа пользователя, но с бэкенда, в каждом поле может прийти еще и null. Чтобы не писать руками, используем маппинг.
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface User {
id: number;
name: string;
email: string;
}
type UserWithNullable = Nullable<User>;
// { id: number | null; name: string | null; email: string | null; }
Чтобы понять что здесь произошло, разберем пример по косточкам
keyof Tвернул нам юнион-тип, состоящий из ключей переданного типа (в данном случае —id | name | email)- Мы проходимся по этому юниону через
P in keyof T, то есть на каждой итерацииPравен очередному ключу переданного типа - Для каждого ключа мы устанавливаем новый тип, который равен либо исходному
T[P], либоnull
Точно так же можно пройтись по типу и добавить или снять какие-то модификаторы. Для снятия просто используем префикс -.
// Делаем поля мутабельными, снимая с них модификатор readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type ImmutableConfig = {
readonly apiUrl: string;
readonly timeout: number;
};
type EditableConfig = Mutable<ImmutableConfig>;
// Представим что из TS пропал Required и напишем свой
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
type PartialUser = {
id?: number;
name?: string;
email?: string;
}
type ReuiredUser = MyRequired<PartialUser>;
Подведем итоги и пойдём работать
Conditional и Mapped Types – это не магия, а инструменты для решения ежедневных задач. Это тот TS, который мы заслужили.
Используя эти инструменты, вы создаете стройную систему типов, которая отражает бизнес-логику приложения. Ваш код становится самодокументируемым, а возможные ошибки отлавливаются на этапе написания (иногда даже на этапе написания типов), а не стреляют на проде вечером пятницы.







