Повышение уровня TypeScript с помощью типов Record
В информатике запись — это структура данных, содержащая набор полей, возможно, разных типов. В TypeScript тип Record
просто позволяет нам определять словари, также называемые парами ключ/значение, с фиксированным типом для ключей и фиксированным типом для значений.
Другими словами, тип Record
позволяет определить тип словаря, то есть имена и типы его ключей. В этой статье мы рассмотрим тип Record
в TypeScript, чтобы лучше понять, что это такое и как он работает. Также рассмотрим, как использовать его для обработки случаев перечисления, как использовать его с дженериками для понимания свойств возвращаемого значения при написании многократно используемого кода.
В чем разница между записью и кортежем
Тип Record
в TypeScript поначалу может показаться несколько нелогичным. В TypeScript записи имеют фиксированное количество членов (т.е. фиксированное количество полей), и эти члены обычно идентифицируются по имени. Это основное отличие записей от кортежей.
Кортежи представляют собой группы упорядоченных элементов, в которых поля идентифицируются по их позиции в определении кортежа. Поля в записях, напротив, имеют имена. Их позиция не имеет значения, поскольку мы можем использовать их имя для ссылки на них.
При этом тип Record
в TypeScript поначалу может показаться непривычным. Вот официальное определение из документации:
.Record<Keys, Type>
строит объектный тип, ключами свойств которого являются Keys
, а значениями свойств — Type
. Эта утилита может быть использована для сопоставления свойств одного типа другому типу
Рассмотрим пример, чтобы лучше понять, как можно использовать тип TypeScript Record
.
Реализация типа Record
Сила типа Record
в том, что с его помощью можно моделировать словари с фиксированным числом ключей. Например, мы можем использовать тип Record
для создания модели университетских курсов:
type Course = "Computer Science" | "Mathematics" | "Literature"
interface CourseInfo {
professor: string
cfu: number
}
const courses: Record<Course, CourseInfo> = {
"Computer Science": {
professor: "Mary Jane",
cfu: 12
},
"Mathematics": {
professor: "John Doe",
cfu: 12
},
"Literature": {
professor: "Frank Purple",
cfu: 12
}
}
В данном примере мы определили тип Course
, который будет перечислять названия занятий. И тип CourseInfo
, который будет содержать некоторые общие сведения о курсах. Затем мы использовали тип Record
для сопоставления каждого курса с его CourseInfo
.
Пока все хорошо; все это выглядит как довольно простой словарь. Настоящая сила типа Record
заключается в том, что TypeScript обнаружит, если мы пропустили Course
.
Идентификация недостающих свойств
Допустим, мы не включили запись для свойства Literature
; при компиляции мы получим следующую ошибку: Свойство
.Literature
отсутствует в типе { "Computer Science": { professor: string; cfu: number; }; Mathematics: { professor: string; cfu: number; }; }; },
но обязательно в типе Record<Course, CourseInfo>
В этом примере TypeScript явно говорит нам, что Literature
отсутствует.
Идентификация неопределённых свойств
TypeScript также обнаружит, если мы добавим записи для значений, которые не определены в Course
. Допустим, мы добавили ещё одну запись в Course
для класса History
. Поскольку мы не включили History
как тип Course
, то получим следующую ошибку компиляции: Объектный литерал может указывать только известные свойства, а
.History
не существует в типе Record<Course, CourseInfo>
Доступ к данным Record
Мы можем получить доступ к данным, относящимся к каждому Course
, как и к любому другому словарю:
Приведённое выше утверждение выводит следующий результат:
{ "teacher": "Frank Purple", "cfu": 12 }
Пример использования: Обеспечение исчерпывающей обработки case
При написании современных приложений часто возникает необходимость запускать различную логику на основе некоторого дискриминирующего значения. Идеальным примером является паттерн проектирования фабрика, когда мы создаём экземпляры различных объектов на основе некоторых входных данных. В этом сценарии обработка всех случаев имеет первостепенное значение.
Самым простым (и в чем-то наивным) решением было бы использование конструкции switch
для обработки всех case
:
Однако если мы добавим в Discriminator
новый случай, то благодаря ветви default
TypeScript не сообщит нам, что мы не смогли обработать новый случай в функции-фабрике. Без ветви default
этого бы не произошло; вместо этого TypeScript обнаружил бы, что в Discriminator
было добавлено новое значение.
Мы можем использовать возможности типа Record
, чтобы исправить это:
string> = {
1: () => "1",
2: () => "2",
3: () => "3"
}
return factories[d]()
}
console.log(factory(1))
Новая функция factory
просто определяет Record
, соответствующую Discriminator
, с помощью специальной функции инициализации, которая не вводит никаких аргументов и возвращает строку. Затем фабрика просто получает нужную функцию, основанную на d: Discriminator
, и возвращает строку, вызывая полученную функцию. Если теперь мы добавим в Discriminator
больше элементов, то тип Record
обеспечит обнаружение TypeScript пропущенных case в фабриках.
Пример использования: Обеспечение проверки типов в приложениях, использующих дженерики
Дженерики позволяют писать код, абстрагированный от реальных типов. Например, Record<K, V>
— это дженерик. При его использовании мы должны выбрать два реальных типа: один для ключей (K
) и один для значений (V
).
В современном программировании дженерики чрезвычайно полезны, поскольку позволяют писать многократно используемый код. Код для выполнения HTTP-вызовов или запросов к базе данных обычно является дженериком по типу возвращаемого значения. Это очень хорошо, но за это приходится расплачиваться, так как нам трудно узнать реальные свойства возвращаемого значения.
Это можно решить, используя тип Record
:
> {
constructor(
public readonly properties: Record<
keyof Properties,
Properties[keyof Properties]
>
) {}
}
Result
является несколько сложным. В данном примере мы объявляем его как общий тип, где параметр типа, Properties
, по умолчанию принимает значение Record<string, any>
.
Использование any
может показаться некрасивым, но на самом деле в этом есть смысл. Как мы увидим через некоторое время, в Record
имена свойств будут сопоставляться со значениями свойств, поэтому мы не можем заранее знать тип свойств. Более того, чтобы сделать его максимально многоразовым, нам придётся использовать самый абстрактный тип, который есть в TypeScript, — any
, действительно!
В конструкторе используется синтаксический сахар TypeScript для определения свойства, доступного только для чтения, которое мы метко назвали properties
. Обратите внимание на определение типа Record
:
- Тип ключа —
keyof Properties
, что означает, что ключи в каждом объекте должны совпадать с ключами, определяемыми типом дженерикProperties
. - Значением каждого из ключей будет значение соответствующего свойства
Properties Record
Теперь, когда мы определили наш основной тип-обёртку, мы можем поэкспериментировать с ним. Следующий пример очень прост, но он демонстрирует, как можно использовать Result
для того, чтобы TypeScript проверял свойства типа дженерик:
>({
title: "Literature",
professor: "Mary Jane",
cfu: 12
})
console.log(course.properties.title)
//console.log(course.properties.students) <- это не компилируется!
В приведённом выше коде мы определяем интерфейс CourseInfo
, который выглядит аналогично тому, что мы видели ранее. Он просто моделирует основную информацию, которую мы хотим хранить и запрашивать: название класса, имя professor
и количество кредитов.
Далее мы смоделируем создание course
. Это просто буквальное значение, но можно представить его как результат запроса к базе данных или HTTP-вызова.
Обратите внимание, что мы можем обращаться к свойствам course
безопасным для типов образом. Когда мы ссылаемся на существующее свойство, например, title
, оно компилируется и работает, как и ожидалось. Когда мы пытаемся получить доступ к несуществующему свойству, например students
, TypeScript обнаруживает, что это свойство отсутствует в объявлении CourseInfo
, и вызов не компилируется.
Это мощная возможность, которую мы можем использовать в своём коде, чтобы убедиться, что значения, получаемые из внешних источников, соответствуют ожидаемому набору свойств. Обратите внимание, что если бы course
имел больше свойств, чем те, которые определены в CourseInfo
, мы все равно могли бы получить к ним доступ. Другими словами, следующий фрагмент будет работать:
>({
title: "Literature",
professor: "Mary Jane",
cfu: 12,
webpage: "https://..."
})
console.log(course.properties.webpage)
Заключение
В этой статье мы рассмотрели один из встроенных типов TypeScript — Record<K, V>
. Мы рассмотрели основные варианты использования типа Record
и изучили его поведение. Затем мы рассмотрели два наиболее ярких случая использования типа Record
.
В первом примере мы исследовали использование типа Record
для того, чтобы TypeScript гарантировал обработку случаев/case перечисления. Во втором примере мы исследовали принудительную проверку типов свойств произвольного объекта в приложении с типами дженерик.
Тип Record
является действительно мощным. Некоторые случаи его использования довольно нишевые, но он обеспечивает реальную ценность для кода нашего приложения.