Record (запись) помогает создавать словари, также называемые парами ключ/значение, с фиксированным типом для ключей и фиксированным типом для значений.
Другими словами, тип Record позволяет определить тип словаря - то есть имена и типы его ключей.
Сила типа Record в том, что с его помощью можно моделировать словари с фиксированным числом ключей.
Анатомия Record - Record<Keys, Type> - строит объектный тип, ключами свойств которого являются Keys, а значениями свойств — Type.
Record для создания модели университетских курсов Link to heading
Еще раз - Record позволяет создавать объекты с динамической структурой, когда мы добавляем в них поля во время исполнения программы; с аннотацией type можно строить динамические key/value (вообще можно любую структуру): у интерфейсов основа - это всегда hard-coded объект.
Ниже приведе пример создания словаря при помощи 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 - для ключей Keys словаря courses. И интерфейс CourseInfo - который будет определять тип значений свойств Type для словаря courses.
Идентификация недостающих свойств Link to heading
Typescript проверяет в создаваемом словаре - наличие всех заранее определенных ключей Keys:
type Course = "Computer Science" | "Mathematics" | "Literature";
… и если при создании словаря будет пропущен один из обязательных ключей - например Literature:
const courses: Record<Course, CourseInfo> = {
"Computer Science": {
professor: "Mary Jane",
cfu: 12
},
"Mathematics": {
professor: "John Doe",
cfu: 12
}
}
… то появится ошибка с предупреждением об отсутствии обязательного ключа Literature в словаре:
Property 'Literature' is missing in type '{ "Computer Science": { professor: string; cfu: number; }; Mathematics: { professor: string; cfu: number; }; }' but required in type 'Record<Course, CourseInfo>'.(2741)
Также Typescript проверяет, чтобы в создаваемом словаре не было ключей Keys, которые не определены заранее. То есть, если сделать так:
const courses: Record<Course, CourseInfo> = {
"Computer Science": {
professor: "Mary Jane",
cfu: 12
},
"Mathematics": {
professor: "Mary Lou",
cfu: 12
},
"Literature": {
professor: "Frank Purple",
cfu: 12
},
"Geography": {
professor: "Ivan Drago",
cfu: 13
}
}
… то получим ошибку с предупреждением, что создаваемый словарь может содержать только известные свойства и ключ Geography не существует в типе Course:
Object literal may only specify known properties, and '"Geography"' does not exist in type 'Record<Course, CourseInfo>'.(2353)
(property) "Geography": {
professor: string;
cfu: number;
}
Доступ к данным Record Link to heading
Получить значение из словаря можно по его ключу. Например, если обратиться к ключу Literature:
courses['Mathematics']
… получим значение, храняящееся в нем:
{
professor: "Mary Lou",
cfu: 12
}
Допустимые типы для ключей Link to heading
Типами для ключей Keys в Record могут быть только - number, string и symbol. В приведенном ниже примере использование для ключа Key типа boolean или object приведет к ошибке:
type numberedUser = Record<number, TUser>;
type stringUser = Record<string, TUser>;
type symbolUser = Record<symbol, TUser>;
type booleanUser = Record<boolean, TUser>; // Type 'boolean' does not satisfy the constraint 'string | number | symbol'
type booleanUser = Record<object, TUser>; // Type 'object' does not satisfy the constraint 'string | number | symbol'
Допустимые типы для значений Link to heading
Для значений Type допустим любой тип данных - наиболее частый случай, это объекты или функции. Это означает, что значениями могут быть компоненты.
Примеры из практики Link to heading
Пример ниже - Record имеет в качестве ключа string, а в качестве значения ключа - целый набор типов:
private get params(): Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>> {
return {
account: this.account,
department: this.department,
documentNumber: this.document.number,
start: this.period.startDate,
end: this.period.endDate,
search: this.search,
status: this.status
};
}
… обратим внимание на запись ReadonlyArray<string | number | boolean> - в данном случае расширенный тип ReadonlyArray<T> предназначен для создания неизменяемого массива; более того, у такого массива отсутствуют методы для добавления, удаления или изменения элементов такого массива, в отличие от обычного массива Array<T>.
Еще один пример динамического создания объекта:
public readonly refs: {
meterStates: typeof CspMesMeterState;
meterStateNames: Record<CspMesMeterState, string>;
} = {
meterStates: CspMesMeterState,
meterStateNames: {
[CspMesMeterState.OK]: 'Работает, нет несоответствия',
[CspMesMeterState.DEFECTIVE_METER]: 'Неисправный ЭС',
[CspMesMeterState.ABSENT_METER]: 'Отсутствует ЭС',
[CspMesMeterState.THEFT_METER]: 'Хищение ЭС',
[CspMesMeterState.DEFECTIVE_SYSTEM]: 'Система неисправна',
[CspMesMeterState.VIOLATION_ACT]: 'Нарушение учета с Актом',
[CspMesMeterState.EXPIRED_INTERVAL]: 'Истек срок МПИ',
[CspMesMeterState.DEFECTIVE_METER_WITH_EXPIRED_INTERVAL]: 'Нерабочий ПУ с истекшим сроком МПИ'
}
};
… здесь CspMesMeterState является перечисляемым типом enum. Думаю, данный кейс является ярким примером создания словаря - четко видно структуру.
Достаточно интересный пример, когда используются сразу два типа - Partial и Record:
public readonly contactTypes: {
[key in AccountUnit]: Partial<Record<ContactType, string>>;
} = {
[AccountUnit.supervisor]: {
[ContactType.ADDRESS]: 'Адрес отделения',
[ContactType.EMAIL]: 'Электронная почта отделения',
[ContactType.PHONE]: 'Телефон отделения'
},
[AccountUnit.curator]: {
[ContactType.ADDRESS]: 'Адрес куратора',
[ContactType.EMAIL]: 'Электронная почта куратора',
[ContactType.PHONE]: 'Телефон куратора'
}
};
… здесь Partial<Record<ContactType, string>> - здесь динамически создается объект Record<ContactType, string>, у которого все поля делаются необязательными при помощи типа Partial.
В заключение еще пример - ничего особенного:
private static readonly refs: Record<BankingProducts, ProductCard> = {
[BankingProducts.virtualCard]: {
title: 'Виртуальная карта',
button: {
label: 'Оформить',
path: ['/credit-calc']
}
},
[BankingProducts.mortgage]: {
title: 'Заявка на ипотеку',
button: {
label: 'Заполнить анкету',
path: ['/', APPEALS_ROUTE.root, APPEALS_ROUTE.predefined.mortgage]
}
},
[BankingProducts.creditCalc]: {
title: 'Расчёт кредита',
button: {
label: 'Рассчитать',
path: [`${PARTNERS_ROUTE_PARAMS.products}/${BANKING_PRODUCTS_ROUTE_PARAMS.creditCalc}`]
}
},
[BankingProducts.pacl]: {
title: 'Предодобренный кредит',
button: {
label: 'Подробнее',
path: ['/', VTB_ROUTE.root, VTB_ROUTE.pacls]
}
}
};
… BankingProducts - это enum; ProductCard - это интерфейс.