Readonly, Partial, Required, Pick, Record¶
Чтобы сделать повседневные будни разработчика немного легче, TypeScript реализовал несколько предопределенных сопоставимых типов, как - Readonly<T>
, Partial<T>
, Required<T>
, Pick<T, K>
и Record<K, T>
. За исключением Record<K, T>
, все они являются так называемыми гомоморфными типами (homomorphic types). Простыми словами, гомоморфизм — это возможность изменять функционал, сохраняя первоначальные свойства всех операций. Если на данный момент это кажется сложным, то текущая глава покажет, что за данным термином не скрывается ничего сложного. Кроме того, в ней будет подробно рассмотрен каждый из перечисленных типов.
Readonly (сделать члены объекта только для чтения)¶
Сопоставимый тип Readonly<T>
добавляет каждому члену объекта модификатор readonly
, делая их тем самым доступными только для чтения.
// lib.es6.d.ts
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Наиболее частое применение данного типа можно встретить при определении функций и методов, параметры которых принадлежат к объектным типам. Поскольку объектные типы передаются по ссылке, то с высокой долей вероятности случайное изменение члена объекта может привести к непредсказуемым последствиям.
interface IPerson {
name: string;
age: number;
}
/**
* Функция, параметр которой не
* защищен от случайного изменения.
*
* Поскольку объектные типы передаются
* по ссылке, то с высокой долей вероятности
* случайное изменение поля name нарушит ожидаемый
* ход выполнения программы.
*/
function mutableAction(person: IPerson) {
person.name = 'NewName'; // Ok
}
/**
* Надежная функция, защищающая свои
* параметры от изменения, не требуя описания
* нового неизменяемого типа.
*/
function immutableAction(person: Readonly<IPerson>) {
person.name = 'NewName'; // Error -> Cannot assign to 'name' because it is a read-only property.
}
Тип сопоставления Readonly<T>
является гомоморфным и добавляя свой модификатор readonly
не влияет на уже существующие модификаторы. Сохранение исходным типом своих первоначальных характеристик (в данном случае — модификаторы), делает сопоставленный тип Readonly<T>
гомоморфным.
interface IPerson {
gender?: string;
}
type Person = Readonly<IPerson>; // type Person = { readonly gender?: string; }
В качестве примера можно привести часто встречающейся на практике случай, в котором универсальный интерфейс описывает объект, предназначенный для работы с данными. Поскольку в львиной доле данные представляются объектными типами, интерфейс декларирует их как неизменяемые, что в дальнейшем при его реализации избавит разработчика от типизации конструкций и тем самым сэкономит для него время на более увлекательные задачи.
/**
* Интерфейс необходим для описания экземпляра
* провайдеров, с которыми будет сопряжено
* приложение. Кроме того, интерфейс описывает
* поставляемые данные как только для чтения,
* что в будущем может сэкономить время.
*/
interface IDataProvider<OutputData, InputData = null> {
getData(): Readonly<OutputData>;
}
/**
* Абстрактный класс описание, определяющий
* поле data, доступный только потомкам как
* только для чтения. Это позволит предотвратить
* случайное изменение данных в классах потомках.
*/
abstract class DataProvider<InputData, OutputData = null>
implements IDataProvider<InputData, OutputData> {
constructor(protected data?: Readonly<OutputData>) {}
abstract getData(): Readonly<InputData>;
}
interface IPerson {
firstName: string;
lastName: string;
}
interface IPersonDataProvider {
name: string;
}
class PersonDataProvider extends DataProvider<
IPerson,
IPersonDataProvider
> {
getData() {
/**
* Работая в теле потомков DataProvider,
* будет не так просто случайно изменить
* данные, доступные через ссылку this.data
*/
let [firstName, lastName] = this.data.name.split(` `);
let result = { firstName, lastName };
return result;
}
}
let provider = new PersonDataProvider({
name: `Ivan Ivanov`,
});
Partial (сделать все члены объекта необязательными)¶
Сопоставимый тип Partial<T>
добавляет членам объекта модификатор ?:
, делая их таким образом необязательными.
// lib.es6.d.ts
type Partial<T> = {
[P in keyof T]?: T[P];
};
Тип сопоставления Partial<T>
является гомоморфным и не влияет на существующие модификаторы, а лишь расширяет модификаторы конкретного типа.
interface IPerson {
readonly name: string; // поле помечено как только для чтения
}
/**
* добавлен необязательный модификатор
* и при этом сохранен модификатор readonly
*
* type Person = {
* readonly name?: string;
* }
*/
type Person = Partial<IPerson>;
Представьте приложение, зависящее от конфигурации, которая как полностью, так и частично, может быть переопределена пользователем. Поскольку работоспособность приложения завязана на конфигурации, члены, определенные в типе, представляющем её, должны быть обязательными. Но поскольку пользователь может переопределить лишь часть конфигурации, функция, выполняющая её слияние с конфигурацией по умолчанию, не может указать в аннотации типа уже определенный тип, так как его члены обязательны. Описывать новый тип слишком утомительно. В таких случаях необходимо прибегать к помощи Partial<T>
.
interface IConfig {
domain: string;
port: '80' | '90';
}
const DEFAULT_CONFIG: IConfig = {
domain: `https://domain.com`,
port: '80',
};
function createConfig(config: IConfig): IConfig {
return Object.assign({}, DEFAULT_CONFIG, config);
}
/**
* Error -> Поскольку в типе IConfig все
* поля обязательные, данную функцию
* не получится вызвать с частичной конфигурацией.
*/
createConfig({
port: '80',
});
function createConfig(config: Partial<IConfig>): IConfig {
return Object.assign({}, DEFAULT_CONFIG, config);
}
/**
* Ok -> Тип Partial<T> сделал все члены,
* описанные в IConfig необязательными,
* поэтому пользователь может переопределить
* конфигурацию частично.
*/
createConfig({
port: '80',
});
Required (сделать все необязательные члены обязательными)¶
Сопоставимый тип Required<T>
удаляет все необязательные модификаторы ?:
, приводя члены объекта к обязательным. Достигается это путем удаления необязательных модификаторов при помощи механизма префиксов - и + рассматриваемого в главе Оператор keyof, Lookup Types, Mapped Types, Mapped Types - префиксы + и -).
type Required<T> = {
[P in keyof T]-?: T[P];
};
Тип сопоставления Required<T>
является полной противоположностью типу сопоставления Partial<T>
.
interface IConfig {
domain: string;
port: '80' | '90';
}
/**
* Partial добавил членам IConfig
* необязательный модификатор ->
*
* type T0 = {
* domain?: string;
* port?: "80" | "90";
* }
*/
type T0 = Partial<IConfig>;
/**
* Required удалил необязательные модификаторы
* у типа T0 ->
*
* type T1 = {
* domain: string;
* port: "80" | "90";
* }
*/
type T1 = Required<T0>;
Тип сопоставления Required<T>
является гомоморфным и не влияет на модификаторы, отличные от необязательных.
interface IT {
readonly a?: number;
readonly b?: string;
}
/**
* Модификаторы readonly остались
* на месте ->
*
* type T0 = {
* readonly a: number;
* readonly b: string;
* }
*/
type T0 = Required<IT>;
Pick (отфильтровать объектный тип)¶
Сопоставимый тип Pick<T, K>
предназначен для фильтрации объектного типа, ожидаемого в качестве первого параметра типа. Фильтрация происходит на основе ключей, представленных множеством литеральных строковых типов, ожидаемых в качестве второго параметра типа.
// lib.es6.d.ts
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Простыми словами, результатом преобразования Pick<T, K>
будет являться тип, состоящий из членов первого параметра, идентификаторы которых указаны во втором параметре.
interface IT {
a: number;
b: string;
c: boolean;
}
/**
* Поле "с" отфильтровано ->
*
* type T0 = { a: number; b: string; }
*/
type T0 = Pick<IT, 'a' | 'b'>;
Стоит заметить, что в случае указания несуществующих ключей возникнет ошибка.
interface IT {
a: number;
b: string;
c: boolean;
}
/**
* Error ->
*
* Type '"a" | "U"' does not satisfy the constraint '"a" | "b" | "c"'.
* Type '"U"' is not assignable to type '"a" | "b" | "c"'.
*/
type T1 = Pick<IT, 'a' | 'U'>;
Тип сопоставления Pick<T, K>
является гомоморфным и не влияет на существующие модификаторы, а лишь расширяет модификаторы конкретного типа.
interface IT {
readonly a?: number;
readonly b?: string;
readonly c?: boolean;
}
/**
* Модификаторы readonly и ? сохранены ->
*
* type T2 = { readonly a?: number; }
*/
type T2 = Pick<IT, 'a'>;
Примером, который самым первым приходит в голову, является функция pick
, в задачу которой входит создавать новый объект путем фильтрации членов существующего.
function pick<T, K extends string & keyof T>(
object: T,
...keys: K[]
) {
return Object.entries(object) // преобразуем объект в массив [идентификатор, значение]
.filter(([key]: Array<K>) => keys.includes(key)) // фильтруем
.reduce(
(result, [key, value]) => ({
...result,
[key]: value,
}),
{} as Pick<T, K>
); // собираем объект из прошедших фильтрацию членов
}
let person = pick(
{
a: 0,
b: ``,
c: true,
},
`a`,
`b`
);
person.a; // Ok
person.b; // Ok
person.c; // Error -> Property 'c' does not exist on type 'Pick<{ a: number; b: string; c: boolean; }, "a" | "b">'.
Record (динамически определить поле в объектном типе)¶
Сопоставимый тип Record<K, T>
предназначен для динамического определения полей в объектном типе. Данный тип определяет два параметра типа. В качестве первого параметра ожидается множество ключей, представленных множеством string
или Literal String
- Record<"a", T>
или Record<"a" | "b", T>
. В качестве второго параметра ожидается конкретный тип данных, который будет ассоциирован с каждым ключом.
// lib.es6.d.ts
type Record<K extends string, T> = {
[P in K]: T;
};
Самый простой пример, который первым приходит в голову, это замена индексных сигнатур.
/**
* Поле payload определено как объект
* с индексной сигнатурой, что позволит
* динамически записывать в него поля.
*/
interface IConfigurationIndexSignature {
payload: {
[key: string]: string;
};
}
/**
* Поле payload определено как
* Record<string, string>, что аналогично
* предыдущему варианту, но выглядит более
* декларативно.
*/
interface IConfigurationWithRecord {
payload: Record<string, string>;
}
let configA: IConfigurationIndexSignature = {
payload: {
a: `a`,
b: `b`,
},
}; // Ok
let configB: IConfigurationWithRecord = {
payload: {
a: `a`,
b: `b`,
},
}; // Ok
Но, в отличие от индексной сигнатуры типа Record<K, T>
может ограничить диапазон ключей.
type WwwConfig = Record<'port' | 'domain', string>;
let wwwConfig: WwwConfig = {
port: '80',
domain: 'https://domain.com',
user: 'User', // Error -> Object literal may only specify known properties, and 'user' does not exist in type 'Record<"port" | "domain", string>'.
};
В данном случае было бы даже более корректным использовать Record<K, T>
в совокупности с ранее рассмотренным типом Partial<T>
.
type WwwConfig = Partial<Record<'port' | 'domain', string>>;
let wwwConfig: WwwConfig = {
port: '80',
// Ok -> поле domain теперь необязательное
user: 'User', // Error -> Object literal may only specify known properties, and 'user' does not exist in type 'Record<"port" | "domain", string>'.
};
Также не будет лишним упомянуть, что поведение данного типа при определении в объекте с предопределенными членами, идентификаторы которых ассоциированы с типами, отличными от типа, указанного в качестве второго параметра, идентично поведению индексной сигнатуры. Напомню, что при попытке определить в объекте члены, идентификаторы которых будут ассоциированы с типами, отличными от указанных в индексной сигнатуре, возникнет ошибка.
/**
* Ok -> поле a ассоциированно с таким
* же типом, что указан в индексной сигнатуре.
*/
interface T0 {
a: number;
[key: string]: number;
}
/**
* Error -> тип поля a не совпадает с типом,
* указанным в индексной сигнатуре.
*/
interface T1 {
a: string; // Error -> Property 'a' of type 'string' is not assignable to string index type 'number'.
[key: string]: number;
}
Данный пример можно переписать с использованием типа пересечения.
interface IValue {
a: number;
}
interface IDynamic {
[key: string]: string;
}
type T = IDynamic & IValue;
/**
* Error ->
* Type '{ a: number; }' is not assignable to type 'IDynamic'.
* Property 'a' is incompatible with index signature.
* Type 'number' is not assignable to type 'string'.
*/
let t: T = {
a: 0,
};
Аналогичное поведение будет и для пересечения, определяемого типом Record<K, T>
.
interface IValue {
a: number;
}
type T = Record<string, string> & IValue;
/**
* Error ->
* Type '{ a: number; }' is not assignable to type 'Record<string, string>'.
* Property 'a' is incompatible with index signature.
* Type 'number' is not assignable to type 'string'.
*/
let t: T = {
a: 0,
};