Использовали ли вы когда-нибудь такие типы утилит, как Partial, Required, Readonly и Pick?

Знаете ли вы, как они работают? Если хотите освоить их и создавать собственные типы утилит, непременно ознакомьтесь с этой статьей.
Регистрация пользователей — широко распространенный сценарий в повседневной работе. В данном случае мы можем использовать TypeScript для определения типа User
, в котором все ключи являются обязательными.
type User = {
name: string;
password: string;
address: string;
phone: string;
};
Как правило, зарегистрированным пользователям разрешается изменять только определенные пользовательские данные. В настоящий момент мы можем определить новый тип UserPartial
, который представляет тип объекта user
, подлежащий обновлению. В нем все ключи являются опциональными.
type UserPartial = {
name?: string;
password?: string;
address?: string;
phone?: string;
};
В случае сценария просмотра информации о пользователе мы ожидаем, что все ключи в типе object
, соответствующем объекту user
, будут доступны только для чтения. Для этого требования можно определить тип ReadonlyUser
.
type ReadonlyUser = {
readonly name: string;
readonly password: string;
readonly address: string;
readonly phone: string;
};
Присмотревшись к трем уже определенным типам, связанным с пользователем, вы увидите, что они содержат много дублирующегося кода.

Как же уменьшить количество дублирующегося кода в приведенных выше типах? С помощью сопоставимых типов! Это общие типы, которые помогут сопоставить исходный тип object
с новым типом object
.


Синтаксис для сопоставимых типов выглядит следующим образом:

Здесь P in K
аналогично утверждению for...in
в JavaScript, которое используется для перебора всех типов в типе K и переменной типа T, применяемой для представления любого типа в TypeScript.

В процессе сопоставления можно также использовать дополнительные read-only-модификаторы и вопросительный знак (?
). Соответствующие модификаторы добавляются и удаляются при помощи префиксов плюс (+
) и минус (-
). По умолчанию используется знак плюс, если отсутствует префикс.
Вот общая картина синтаксиса распространенных сопоставимых типов:
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
Теперь рассмотрим несколько примеров.

Переопределим тип UserPartial
с помощью сопоставимых типов.
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
В приведенном выше коде мы определяем сопоставимый тип MyPartial
, а затем используем его для сопоставления типа User
с типом UserPartial
. Оператор keyof
применяется для получения всех ключей типа, а его возвращаемый тип представляет собой тип union
. Переменная типа P изменяется на другой тип при каждом обходе (T[P]
), что напоминает синтаксис доступа к атрибутам, и используется для получения типа значения, соответствующего атрибуту типа object
.
Продемонстрируем полный поток выполнения сопоставимого типа MyPartial
. Вы можете посмотреть его несколько раз, чтобы лучше понять работу сопоставимого типа TypeScript.

TypeScript 4.1 позволяет перераспределять ключи в сопоставимых типах с помощью выражения as
. Его синтаксис выглядит следующим образом:
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// New Syntax! (Новый синтаксис!)
}
Здесь тип NewKeyType
должен быть подтипом типа union string | number | symbol
. Используя выражение as
, мы можем определить тип утилиты Getters
, который генерирует соответствующий тип Getter
для типа object
.
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }; interface Person { name: string; age: number; location: string; } type LazyPerson = Getters<Person>; // { // getName: () => string; // getAge: () => number; // getLocation: () => string; // }

Обратите внимание на приведенный выше код. Поскольку тип, возвращаемый keyof T
, может содержать тип symbol
, а тип утилиты Capitalize
требует, чтобы обрабатываемый тип был подтипом строкового типа, необходима фильтрация типов с помощью оператора &
.
Кроме того, в процессе перераспределения ключей мы можем фильтровать ключи, возвращая тип never
.
// Удалите свойство 'kind' type RemoveKindField<T> = { [K in keyof T as Exclude<K, "kind">]: T[K] }; interface Circle { kind: "circle"; radius: number; } type KindlessCircle = RemoveKindField<Circle>; // type KindlessCircle = { // radius: number; // };

На этом все. Уверен, что эта статья помогла вам понять, какова функция сопоставимых типов и как реализованы некоторые типы утилит в TypeScript.
Читайте также:
- Как улучшить код на TypeScript: 5 рекомендаций
- Прототипирование без API
- 5 советов о браузерных инструментах разработчика
Читайте нас в Telegram, VK и Дзен
Перевод статьи Bytefer: Use TypeScript Mapped Types Like a Pro