Использовали ли вы когда-нибудь такие типы утилит, как 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.

Сопоставление опциональных свойств
Сопоставление свойства readonly

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

Синтаксис сопоставимых типов

Здесь 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.

Поток выполнения MyPartial

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.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Bytefer: Use TypeScript Mapped Types Like a Pro

Предыдущая статьяКак оркестровать микросервисы с помощью Docker Compose
Следующая статьяПокрытие кода в Rust