Чтобы сделать повседневные будни разработчика немного легче, TypeScript реализовал несколько предопределенных сопоставимых типов, как - Readonly<T>, Partial<T>, Required<T>, Pick<T, K> и Record<K, T>. За исключением Record<K, T>, все они являются так называемыми гомоморфными типами (homomorphic types). Простыми словами, гомоморфизм — это возможность изменять функционал, сохраняя первоначальные свойства всех операций. Если на данный момент это кажется сложным, то текущая глава покажет, что за данным термином не скрывается ничего сложного. Кроме того, в ней будет подробно рассмотрен каждый из перечисленных типов.
Readonly (сделать члены объекта только для чтения)¶
Сопоставимый тип Readonly<T> добавляет каждому члену объекта модификатор readonly, делая их тем самым доступными только для чтения.
Наиболее частое применение данного типа можно встретить при определении функций и методов, параметры которых принадлежат к объектным типам. Поскольку объектные типы передаются по ссылке, то с высокой долей вероятности случайное изменение члена объекта может привести к непредсказуемым последствиям.
interfaceIPerson{name:string;age:number;}/** * Функция, параметр которой не * защищен от случайного изменения. * * Поскольку объектные типы передаются * по ссылке, то с высокой долей вероятности * случайное изменение поля name нарушит ожидаемый * ход выполнения программы. */functionmutableAction(person:IPerson){person.name='NewName';// Ok}/** * Надежная функция, защищающая свои * параметры от изменения, не требуя описания * нового неизменяемого типа. */functionimmutableAction(person:Readonly<IPerson>){person.name='NewName';// Error -> Cannot assign to 'name' because it is a read-only property.}
Тип сопоставления Readonly<T> является гомоморфным и добавляя свой модификатор readonly не влияет на уже существующие модификаторы. Сохранение исходным типом своих первоначальных характеристик (в данном случае — модификаторы), делает сопоставленный тип Readonly<T> гомоморфным.
12345
interfaceIPerson{gender?:string;}typePerson=Readonly<IPerson>;// type Person = { readonly gender?: string; }
В качестве примера можно привести часто встречающейся на практике случай, в котором универсальный интерфейс описывает объект, предназначенный для работы с данными. Поскольку в львиной доле данные представляются объектными типами, интерфейс декларирует их как неизменяемые, что в дальнейшем при его реализации избавит разработчика от типизации конструкций и тем самым сэкономит для него время на более увлекательные задачи.
/** * Интерфейс необходим для описания экземпляра * провайдеров, с которыми будет сопряжено * приложение. Кроме того, интерфейс описывает * поставляемые данные как только для чтения, * что в будущем может сэкономить время. */interfaceIDataProvider<OutputData,InputData=null>{getData():Readonly<OutputData>;}/** * Абстрактный класс описание, определяющий * поле data, доступный только потомкам как * только для чтения. Это позволит предотвратить * случайное изменение данных в классах потомках. */abstractclassDataProvider<InputData,OutputData=null>implementsIDataProvider<InputData,OutputData>{constructor(protecteddata?:Readonly<OutputData>){}abstractgetData():Readonly<InputData>;}interfaceIPerson{firstName:string;lastName:string;}interfaceIPersonDataProvider{name:string;}classPersonDataProviderextendsDataProvider<IPerson,IPersonDataProvider>{getData(){/** * Работая в теле потомков DataProvider, * будет не так просто случайно изменить * данные, доступные через ссылку this.data */let[firstName,lastName]=this.data.name.split(` `);letresult={firstName,lastName};returnresult;}}letprovider=newPersonDataProvider({name:`Ivan Ivanov`,});
Partial (сделать все члены объекта необязательными)¶
Сопоставимый тип Partial<T> добавляет членам объекта модификатор ?:, делая их таким образом необязательными.
Тип сопоставления Partial<T> является гомоморфным и не влияет на существующие модификаторы, а лишь расширяет модификаторы конкретного типа.
1 2 3 4 5 6 7 8 910111213
interfaceIPerson{readonlyname:string;// поле помечено как только для чтения}/** * добавлен необязательный модификатор * и при этом сохранен модификатор readonly * * type Person = { * readonly name?: string; * } */typePerson=Partial<IPerson>;
Представьте приложение, зависящее от конфигурации, которая как полностью, так и частично, может быть переопределена пользователем. Поскольку работоспособность приложения завязана на конфигурации, члены, определенные в типе, представляющем её, должны быть обязательными. Но поскольку пользователь может переопределить лишь часть конфигурации, функция, выполняющая её слияние с конфигурацией по умолчанию, не может указать в аннотации типа уже определенный тип, так как его члены обязательны. Описывать новый тип слишком утомительно. В таких случаях необходимо прибегать к помощи Partial<T>.
interfaceIConfig{domain:string;port:'80'|'90';}constDEFAULT_CONFIG:IConfig={domain:`https://domain.com`,port:'80',};functioncreateConfig(config:IConfig):IConfig{returnObject.assign({},DEFAULT_CONFIG,config);}/** * Error -> Поскольку в типе IConfig все * поля обязательные, данную функцию * не получится вызвать с частичной конфигурацией. */createConfig({port:'80',});functioncreateConfig(config:Partial<IConfig>):IConfig{returnObject.assign({},DEFAULT_CONFIG,config);}/** * Ok -> Тип Partial<T> сделал все члены, * описанные в IConfig необязательными, * поэтому пользователь может переопределить * конфигурацию частично. */createConfig({port:'80',});
Required (сделать все необязательные члены обязательными)¶
Сопоставимый тип Required<T> удаляет все необязательные модификаторы ?:, приводя члены объекта к обязательным. Достигается это путем удаления необязательных модификаторов при помощи механизма префиксов - и + рассматриваемого в главе Оператор keyof, Lookup Types, Mapped Types, Mapped Types - префиксы + и -).
123
typeRequired<T>={[PinkeyofT]-?:T[P];};
Тип сопоставления Required<T> является полной противоположностью типу сопоставления Partial<T>.
Сопоставимый тип Pick<T, K> предназначен для фильтрации объектного типа, ожидаемого в качестве первого параметра типа. Фильтрация происходит на основе ключей, представленных множеством литеральных строковых типов, ожидаемых в качестве второго параметра типа.
Простыми словами, результатом преобразования Pick<T, K> будет являться тип, состоящий из членов первого параметра, идентификаторы которых указаны во втором параметре.
1 2 3 4 5 6 7 8 9101112
interfaceIT{a:number;b:string;c:boolean;}/** * Поле "с" отфильтровано -> * * type T0 = { a: number; b: string; } */typeT0=Pick<IT,'a'|'b'>;
Стоит заметить, что в случае указания несуществующих ключей возникнет ошибка.
1 2 3 4 5 6 7 8 910111213
interfaceIT{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"'. */typeT1=Pick<IT,'a'|'U'>;
Тип сопоставления Pick<T, K> является гомоморфным и не влияет на существующие модификаторы, а лишь расширяет модификаторы конкретного типа.
1 2 3 4 5 6 7 8 9101112
interfaceIT{readonlya?:number;readonlyb?:string;readonlyc?:boolean;}/** * Модификаторы readonly и ? сохранены -> * * type T2 = { readonly a?: number; } */typeT2=Pick<IT,'a'>;
Примером, который самым первым приходит в голову, является функция pick, в задачу которой входит создавать новый объект путем фильтрации членов существующего.
functionpick<T,Kextendsstring&keyofT>(object:T,...keys:K[]){returnObject.entries(object)// преобразуем объект в массив [идентификатор, значение].filter(([key]:Array<K>)=>keys.includes(key))// фильтруем.reduce((result,[key,value])=>({...result,[key]:value,}),{}asPick<T,K>);// собираем объект из прошедших фильтрацию членов}letperson=pick({a:0,b:``,c:true,},`a`,`b`);person.a;// Okperson.b;// Okperson.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>. В качестве второго параметра ожидается конкретный тип данных, который будет ассоциирован с каждым ключом.
/** * Поле payload определено как объект * с индексной сигнатурой, что позволит * динамически записывать в него поля. */interfaceIConfigurationIndexSignature{payload:{[key:string]:string;};}/** * Поле payload определено как * Record<string, string>, что аналогично * предыдущему варианту, но выглядит более * декларативно. */interfaceIConfigurationWithRecord{payload:Record<string,string>;}letconfigA:IConfigurationIndexSignature={payload:{a:`a`,b:`b`,},};// OkletconfigB:IConfigurationWithRecord={payload:{a:`a`,b:`b`,},};// Ok
Но, в отличие от индексной сигнатуры типа Record<K, T> может ограничить диапазон ключей.
12345678
typeWwwConfig=Record<'port'|'domain',string>;letwwwConfig: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>.
1234567
typeWwwConfig=Partial<Record<'port'|'domain',string>>;letwwwConfig: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>'.};
Также не будет лишним упомянуть, что поведение данного типа при определении в объекте с предопределенными членами, идентификаторы которых ассоциированы с типами, отличными от типа, указанного в качестве второго параметра, идентично поведению индексной сигнатуры. Напомню, что при попытке определить в объекте члены, идентификаторы которых будут ассоциированы с типами, отличными от указанных в индексной сигнатуре, возникнет ошибка.
1 2 3 4 5 6 7 8 910111213141516171819
/** * Ok -> поле a ассоциированно с таким * же типом, что указан в индексной сигнатуре. */interfaceT0{a:number;[key:string]:number;}/** * Error -> тип поля a не совпадает с типом, * указанным в индексной сигнатуре. */interfaceT1{a:string;// Error -> Property 'a' of type 'string' is not assignable to string index type 'number'.[key:string]:number;}
Данный пример можно переписать с использованием типа пересечения.
1 2 3 4 5 6 7 8 910111213141516171819
interfaceIValue{a:number;}interfaceIDynamic{[key:string]:string;}typeT=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'. */lett:T={a:0,};
Аналогичное поведение будет и для пересечения, определяемого типом Record<K, T>.
1 2 3 4 5 6 7 8 9101112131415
interfaceIValue{a:number;}typeT=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'. */lett:T={a:0,};