Сопоставленные типы в TypeScript
Сопоставленный тип строит новый тип, проходя по каждому свойству существующего типа и изменяя его одинаковым образом.
- Сопоставленные типы = преобразуют каждое свойство типа
- Часто используемые встроенные:
Partial,Readonly,Pick,Omit,Record
Небольшой пример сопоставленного типа
Начиная с типа Person, один сопоставленный тип делает каждое свойство необязательным, а другой делает каждое свойство readonly.
// Small example
type Person = { name: string; age: number };
type PartialPerson = { [P in keyof Person]?: Person[P] };
type ReadonlyPerson = { readonly [P in keyof Person]: Person[P] };
Базовый синтаксис сопоставленного типа
Основной синтаксис сопоставленного типа — { [P in K]: T }, где:
P— имя свойства, которое обрабатывается в данный моментK— объединение имён свойств, по которым нужно пройти (частоkeyof SomeType)T— тип, который каждое свойство получает в новом типе
Пример
PartialPerson добавляет ? к каждому свойству типа Person, поэтому объект только со свойством name допустим. ReadonlyPerson добавляет readonly к каждому свойству, поэтому присваивание значения свойству age после этого было бы ошибкой компиляции.
{ [P in K]: T }, преобразовав интерфейс вручную.
Объявите интерфейс Person со свойствами name: string, age: number и email: string. Постройте сопоставленный тип PartialPerson, который делает все свойства необязательными, и создайте объект partialPerson только со свойством name: "John". Затем постройте сопоставленный тип ReadonlyPerson, который помечает каждое свойство как readonly, создайте полный объект readonlyPerson (Alice, 30, alice@example.com) и оставьте закомментированное присваивание age с пояснением, что это была бы ошибка записи в свойство только для чтения. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// Определяем тип объекта
interface Person {
name: string;
age: number;
email: string;
}
// Создаём сопоставленный тип, который делает все свойства необязательными
type PartialPerson = {
[P in keyof Person]?: Person[P];
};
// Использование
const partialPerson: PartialPerson = {
name: "John"
// age и email необязательны
};
// Создаём сопоставленный тип, который делает все свойства доступными только для чтения
type ReadonlyPerson = {
readonly [P in keyof Person]: Person[P];
};
// Использование
const readonlyPerson: ReadonlyPerson = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// readonlyPerson.age = 31; // Ошибка: нельзя присвоить значение 'age', так как это свойство доступно только для чтения
Встроенные сопоставленные типы
Стандартная библиотека TypeScript уже содержит несколько полезных сопоставленных типов, поэтому самые распространённые из них не нужно писать самому:
Partial<T>: сделать все свойства необязательнымиReadonly<T>: сделать все свойства доступными только для чтенияPick<T, K>: оставить только выбранный набор ключейOmit<T, K>: удалить набор ключейRecord<K, V>: сопоставить набору ключей один тип значения
Пример
Каждый псевдоним ниже применяет к User один встроенный сопоставленный тип. Комментарий после каждого псевдонима показывает получающийся тип объекта. Также показан Required — он противоположен Partial.
User со свойствами id: number, name: string, email: string и isAdmin: boolean, затем создайте псевдонимы типов с помощью Partial<User>, Required<Partial<User>>, Readonly<User>, Pick<User, "email" | "id">, Omit<User, "id" | "isAdmin"> и Record<"admin" | "user" | "guest", string>, снабдив каждый комментарием, в котором расписан эквивалентный тип объекта. Файл только объявляет типы и ничего не выводит.
Вывод: программа на TypeScript и захваченный вывод
interface User {
id: number;
name: string;
email: string;
isAdmin: boolean;
}
// Partial<T> - делает все свойства необязательными
type PartialUser = Partial<User>;
// Эквивалентно: { id?: number; name?: string; email?: string; isAdmin?: boolean; }
// Required<T> - делает все свойства обязательными
type RequiredUser = Required<Partial<User>>;
// Эквивалентно: { id: number; name: string; email: string; isAdmin: boolean; }
// Readonly<T> - делает все свойства доступными только для чтения
type ReadonlyUser = Readonly<User>;
// Эквивалентно: { readonly id: number; readonly name: string; ... }
// Pick<T, K> - создаёт тип с подмножеством свойств из T
type UserCredentials = Pick<User, "email" | "id">;
// Эквивалентно: { email: string; id: number; }
// Omit<T, K> - создаёт тип, удаляя указанные свойства из T
type PublicUser = Omit<User, "id" | "isAdmin">;
// Эквивалентно: { name: string; email: string; }
// Record<K, T> - создаёт тип с указанными ключами и типом значений
type UserRoles = Record<"admin" | "user" | "guest", string>;
// Эквивалентно: { admin: string; user: string; guest: string; }
Создание собственных сопоставленных типов
Вы можете писать собственные обобщённые сопоставленные типы, чтобы преобразовывать типы именно так, как вам нужно.
Пример
StringifyProperties<T> превращает каждое свойство в string. Validator<T> превращает каждое свойство в функцию, которая принимает значение типа этого свойства (T[P]) и возвращает boolean, поэтому productValidator должен содержать по одной проверке на каждое свойство типа Product.
Product (id: number, name: string, price: number, inStock: boolean).
Напишите StringifyProperties<T>, который превращает каждое свойство в string, и Validator<T>, который превращает каждое свойство P в функцию (value: T[P]) => boolean. Примените первый, чтобы получить тип StringProduct, и с помощью второго постройте объект productValidator со следующими проверками: id больше 0, непустое name, price не меньше 0 и inStock является значением типа boolean. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// Базовый интерфейс
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
// Создаём сопоставленный тип, который превращает все свойства в тип string
type StringifyProperties<T> = {
[P in keyof T]: string;
};
// Использование
type StringProduct = StringifyProperties<Product>;
// Эквивалентно: { id: string; name: string; price: string; inStock: string; }
// Создаём сопоставленный тип, который добавляет функцию проверки для каждого свойства
type Validator<T> = {
[P in keyof T]: (value: T[P]) => boolean;
};
// Использование
const productValidator: Validator<Product> = {
id: (id) => id > 0,
name: (name) => name.length > 0,
price: (price) => price >= 0,
inStock: (inStock) => typeof inStock === "boolean"
};
Изменение модификаторов свойств
Сопоставленный тип также может добавлять или удалять модификаторы свойств readonly и ? (необязательное свойство). Запись +readonly или readonly добавляет модификатор, а запись -readonly удаляет его. То же самое работает для ? с помощью -?.
Пример
Mutable<T> использует -readonly, чтобы убрать признак «только для чтения» у каждого свойства типа Configuration. RequiredProps<T> использует -?, чтобы сделать каждое необязательное свойство обязательным.
readonly и ? с помощью -readonly и -?.
Объявите интерфейс Configuration со свойствами readonly apiKey: string, readonly apiUrl: string, timeout?: number и retries?: number. Напишите Mutable<T> (убирает readonly у каждого свойства) и RequiredProps<T> (убирает ? у каждого свойства), примените каждый к Configuration как MutableConfig и RequiredConfig и после каждого напишите комментарий с эквивалентным типом объекта. Файл только объявляет типы и ничего не выводит.
Вывод: программа на TypeScript и захваченный вывод
// Базовый интерфейс с несколькими readonly и необязательными свойствами
interface Configuration {
readonly apiKey: string;
readonly apiUrl: string;
timeout?: number;
retries?: number;
}
// Удаляем модификатор readonly у всех свойств
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Использование
type MutableConfig = Mutable<Configuration>;
// Эквивалентно: { apiKey: string; apiUrl: string; timeout?: number; retries?: number; }
// Делаем все необязательные свойства обязательными
type RequiredProps<T> = {
[P in keyof T]-?: T[P];
};
// Использование
type RequiredConfig = RequiredProps<Configuration>;
// Эквивалентно: { readonly apiKey: string; readonly apiUrl: string; timeout: number; retries: number; }
Продвинутые сопоставленные типы
Сопоставленные типы становятся ещё мощнее, когда вы объединяете их с условными типами. Тогда новый тип каждого свойства может зависеть от прежнего.
Пример
FormattedResponse<T> проверяет каждое свойство с помощью T[P] extends number и превращает числовые свойства в string. StringPropsOnly<T> использует конструкцию as для переименования ключей: свойства, которые не являются строками, сопоставляются с never, что убирает их из результата.
ApiResponse (data: unknown, status: number, message: string, timestamp: number).
Напишите FormattedResponse<T>, который отображает каждое свойство типа number в string, а остальные оставляет без изменений, и StringPropsOnly<T>, который с помощью конструкции переименования ключей as оставляет только свойства с типом string. Примените оба типа к ApiResponse и прокомментируйте получившиеся типы. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// Базовый интерфейс
interface ApiResponse {
data: unknown;
status: number;
message: string;
timestamp: number;
}
// Условный сопоставленный тип: превращает каждое числовое свойство в отформатированную строку
type FormattedResponse<T> = {
[P in keyof T]: T[P] extends number ? string : T[P];
};
// Использование
type FormattedApiResponse = FormattedResponse<ApiResponse>;
// Эквивалентно: { data: unknown; status: string; message: string; timestamp: string; }
// Ещё один пример: отбор только строковых свойств
type StringPropsOnly<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
// Использование
type ApiResponseStringProps = StringPropsOnly<ApiResponse>;
// Эквивалентно: { message: string; }
Ключевые выводы
Сопоставленные типы позволяют единообразно преобразовывать каждое свойство типа.
Ключевые понятия:
- Преобразование типов: массовое изменение типов свойств
- Модификаторы свойств: добавление или удаление
readonlyи? - Переименование ключей: переименование или отбор свойств с помощью конструкции
as - Композиция: сочетание сопоставленных типов с другими возможностями TypeScript, например с условными типами
Типичные сценарии использования:
- Создание версий типов только для чтения
- Превращение всех свойств в необязательные или обязательные
- Преобразование типов свойств (например, в nullable или readonly)
- Отбор свойств по их типам
- Создание типобезопасных вспомогательных функций