Record typescript зачем
Перейти к содержимому

Record typescript зачем

  • автор:

TypeScript Pick и Record

Сравнение на примере GET/POST запросов, обработке ошибок и возможности задавать базовую конфигурацию.

18 февраля 2018 г. в JavaScript

Правильный импорт lodash методов

После анализа bundl’a разрабатываемого приложения меня насторожил момент, что lodash занимает хороший такой кусок в 95kb, хотя используется всего 3 функции. И вот к чему привели поиски.

12 января 2019 г. в Angular, JavaScript

ES6. Union, intersection, difference
  • Пересечение
  • Объединение
  • Разность

06 ноября 2019 г. в JavaScript

Искусство типизации: TypeScript Utility Types

Что вы чувствуете от познания нового? За себя скажу, что в такие моменты просветления меня переполняет неподдельная детская радость от свершившегося открытия. Жаль, что этих моментов становится всё меньше. К чему я это? Когда мне в голову мне пришла мысль о написании статьи на эту тему, я сразу вспомнил то ощущение прозрения, которое испытал в момент открытия Utility Types. Всё сразу встало на свои места, и я понял какого кусочка пазла мне всё это время не хватало. Именно о нём я расскажу далее.

TypeScript Utility Types — это набор встроенных типов, которые можно использовать для манипулирования типами данных в коде. Рассмотрим их подробнее.

Awaited

Awaited — это специальный тип, который может быть использован для обозначения типа, который будет возвращен из асинхронной функции.

async function getData(): Promise  < return 'hello'; >let awaitedData: Awaited>; // теперь awaitedData может быть 'hello'

Partial

Partial — делает все свойства объекта типа T необязательными.

interface Person < name: string; age: number; >let partialPerson: Partial; // теперь partialPerson может быть

Required

Required — делает все свойства объекта типа T обязательными.

interface Person < name?: string; age?: number; >let requiredPerson: Required; // теперь requiredPerson может быть

Readonly

Readonly — делает все свойства объекта типа T доступными только для чтения.

interface Point < x: number; y: number; >let readonlyPoint: Readonly; // теперь readonlyPoint может быть

Record

Record — создает тип, который является записью с ключами, определенными в первом параметре, и значениями типа, определенного во втором параметре.

type Keys = 'a' | 'b' | 'c'; type RecordType = Record; let record: RecordType; // теперь record может быть

Pick

Pick — выбирает свойства объекта типа T с ключами, указанными в K.

interface Person < name: string; age: number; >let pickedPerson: Pick; // теперь pickedPerson может быть

Omit

Omit — выбирает свойства объекта типа T, исключая те, которые указаны в K

interface Person < name: string; age: number; >let omittedPerson: Omit; // теперь omittedPerson может быть

Exclude

Exclude — исключает определенные типы из объединенного типа.

type A = 'a' | 'b' | 'c'; type B = Exclude; // теперь B это 'c'

Extract

Extract — извлекает из типа Type только те типы, которые присутствуют в Union.

type A = 'a' | 'b' | 'c'; type B = 'a' | 'b'; type C = Extract; // теперь C это 'a' | 'b'

NonNullable

NonNullable — извлекает тип из Type, исключая null и undefined.

let value: string | null | undefined; let nonNullableValue: NonNullable; // теперь nonNullableValue это string

Parameters

Parameters — извлекает типы аргументов функции Type.

function foo(a: string, b: number) <> type FooParameters = Parameters; // теперь FooParameters это [string, number]

ConstructorParameters

ConstructorParameters — извлекает типы аргументов конструктора Type.

class Foo < constructor(a: string, b: number) <>> type FooConstructorParameters = ConstructorParameters; // теперь FooConstructorParameters это [string, number]

ReturnType

ReturnType — извлекает тип возвращаемого значения функции Type.

function foo(): string < return 'hello'; >type FooReturnType = ReturnType; // теперь FooReturnType это string

InstanceType

InstanceType — извлекает тип экземпляра класса Type.

class Foo < x: number >type FooInstance = InstanceType; // теперь FooInstance это

ThisParameterType

ThisParameterType — извлекает тип this из функции Type.

class Foo < x: number; method(this: this): void < >> type ThisType = ThisParameterType; // теперь ThisType это Foo

OmitThisParameter

OmitThisParameter — определяет функцию без типа this .

class Foo < x: number; method(this: this): void < >> type MethodType = OmitThisParameter; // теперь MethodType это () => void

ThisType

ThisType — добавляет тип this к функции Type.

class Foo < x: number; method(): void < >> type MethodType = ThisType; // теперь MethodType это (this: Foo) => void

Управление регистром

Uppercase , Lowercase , Capitalize , Uncapitalize — это утилитные типы для манипуляции строками, которые изменяют регистр строки в соответствии с их именем.

type Uppercased = Uppercase; // 'HELLO' type Lowercased = Lowercase; // 'hello' type Capitalized = Capitalize; // 'Hello' type Uncapitalized = Uncapitalize; // 'hello'

Заключение

Кто-то скажет: «Большинство из этого не пригодится в реальной работе» — и будет больше прав, чем не прав. Для того чтобы шлёпать формы или писать CRUD’ы не нужно иметь углублённые знания в построении типов, в то время как решение нетривиальной задачи будет найдено быстрее при наличии компетенций в разных направлениях и практиках.

В основе всей моей деятельности лежит именно этот подход. Пришлось спуститься очень глубоко в мир программирования, чтобы найти ответы. Оттуда я и вещаю на своём телеграмм канале.

  • typescript
  • type safety
  • хреновый программист

TypeScript: Обобщённые типы (Generics)

В двух предыдущих статьях мы говорили о том, что из себя представляет TypeScript, зачем и когда он может вам понадобиться, а также о том, какие типы и структуры будут встречаться вам каждый день при написании кода на этом надмножестве языка JavaScript.

Также, мы вскользь задели тему обобщений, когда разговаривали о типе пересечения. Сегодня мы поговорим об обобщённых типах подробнее.

Первый взгляд на обобщения

Обобщённый тип (обобщение, дженерик) позволяет резервировать место для типа, который будет заменён на конкретный, переданный пользователем, при вызове функции или метода, а также при работе с классами.

Допустим, у нас есть функция, принимающая на вход один аргумент и возвращающая его же без каких-либо изменений. Такая функция присутствует во многих библиотеках функционального программирования и носит имя identity . В голове вы можете держать аналогию с командой echo в Unix или print в Python.

Посмотрим на простейшую реализацию функции, описанной выше, если бы в TypeScript не было обобщённых типов и нам пришлось бы довольствоваться тем, что уже есть.

function identity(arg: any): any

Всё как в описании: функция принимает аргумент любого типа и возвращает его же. Однако, это не совсем так. Внимательно присмотритесь к типам: в примере указано, что функция принимает аргумент какого-то типа и возвращает значение какого-то типа, но при этом они никак не связаны. Грубо говоря, сейчас мы можем передать аргумент типа number и получить значение типа string – это валидно, потому что any подразумевает под собой всё что угодно.

Сейчас функция представляет собой «чёрный ящик», в который с одной стороны что-то входит, а с другой стороны что-то выходит, возможно, похожее на то, что входило или нет – непонятно. Я бы сказал, что такую функцию можно называть «обезличивающей функцией» в контексте типов. При использовании any в качестве типа возвращаемого значения мы обезличиваем результат функции.

Теперь перепишем функцию так, чтобы каждый из нас знал, что тип переданного аргумента является и типом возвращаемого значения.

function identity(arg: T): T

Здесь T – это некоторый параметр-тип T , который будет захвачен при вызове функции. Конструкция после имени функции указывает на то, что эта функция собирается захватить тип и подменить им все T .

Можно использовать любые буквы, которые вам хочется, но буквы на входе и выходе функции должны совпадать, если этого требует логика её работы. Так уж сложилось, что, если имеется единственный параметр-тип, то он получает имя T , но лишь в том случае, если это не нарушает общую ясность. При объявлении нескольких параметр-типов их имена записываются, чаще всего, как T , U и A , соответственно. Однако, существует и другое соглашение, поощряющее именование всех параметр-типов через T , но с применением уточнений, например, TKey , TKeyType или TValue . Официальная документация и разработчики языка придерживаются первого соглашения.

В мире C#, технически, можно утверждать, что identity – это открытый тип, а identity – замкнутый. При этом нужно понимать, что вы можете работать только с замкнутыми типами.

Попробуем вызвать эту функцию с аргументом типа number и поверхностно проследим за тем, как компилятор определяет, что вернёт функция.

const value = identity(115); 

При вызове функции тип заполняет обобщённый параметр T . В этот момент компилятор неявно подставляет вместо T переданный тип number и переходит к валидации типов переданных аргументов, а затем входит внутрь функции и валидирует её тело.

Примечание

Самое часто используемое в TypeScript обобщение – Promise . Например, это обещание, возвращающее строку function a(): Promise < return 'a' >.

Логический вывод обобщённых типов

Многих разработчиков смущает указание типа с помощью знаков «меньше» и «больше». Для упрощения создания, чтения и работы с кодом TypeScript имеет возможность логического вывода типов при вызове обобщённых типов. Это значит, что компилятор будет пытаться определить (логически вывести) тип, который автоматически будет использоваться при вызове обобщённого типа.

const value = identity('string'); 

В отличие от C#, в TypeScript нет возможности написать две функции с одним именем, когда первая функция реализована для конкретного типа, а вторая – для обобщённого. Например, такое в TypeScript написать не получится:

function log(arg: string): void < console.log(arg); >function log(arg: T): void < console.log(arg); >// Error → Duplicate function implementation 

Также стоит упомянуть, что компилятор выдаст ошибку, если он не сможет точно вывести тип для параметр-типа. Для того, чтобы разрешить сложившуюся ситуацию нужно явно указать типы.

function swap(a: T, b: T) < const temp = a; a = b; b = temp; >const a: string = 'string'; const b: number = 123; swap(a, b); // Error → The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly. 

Для чего существуют обобщённые типы

При написании кода вы всегда встаёте перед выбором, имеющим как минимум две ветви решения. Первый путь приводит вас к копированию существующего кода с нужным вам типом, что также приводит к «распуханию» кода и необходимости думать при рефакторинге. Второй путь – унификация, что подразумевает под собой параметрический полиморфизм, в основе которого лежит использование одного и того же кода, но с разными типами. Получается, что обобщённые типы нужны для написания кода, который можно многократно использовать вне зависимости от типов.

Безопасность типов

Когда обобщённый код (например, алгоритм) применяется с конкретным типом, компилятор гарантирует нам, что в алгоритме будут использоваться лишь те «объекты», что совместимы с этим типом данных. В случае, если попытаться передать не совместимый с кодом тип, то компилятор породит ошибку.

Более простой и понятный код

Поскольку компилятор защищает нас от передачи аргументов неправильных типов, это позволяет использовать меньше защитников типов, которые были рассмотрены в предыдущей статье. Однако, работает только в том случае, если ваш стек полностью построен на TypeScript и это не будет работать в том случае, если вы распространяете свой код в скомпилированном виде. В таком случае вы обязаны использовать защитники типов, если хотите оберегать своих пользователей от неправильного использования вашего кода.

Базовые ограничения обобщённых типов

Посмотрим на код ниже и попытаемся ответить на вопрос: что здесь может пойти не так?

function getLength(arg: T): number

Ответ достаточно прост: компилятор ничего не знает про тип аргумента. По умолчанию, вместо обобщённого параметра можно подставить любой тип, поэтому компилятор не знает, что это за тип такой, – T и есть ли у него свойство length . Такая проблема решается на уровне разработчика с помощью ограничений. В нашем случае мы хотим ограничить принимаемое множество типов T условием: наличием свойства length . Для этого нужно создать некоторый интерфейс, где указано нужное свойство и расширить его, используя обобщённый тип T .

interface Lengthwise < length: number; >function getLength(arg: T): number

В коде выше мы оповещаем компилятор о том, что на вход функции getLength могут подаваться аргументы лишь того типа, что имеют свойство length . Такая запись защитит вас от передачи в функции, например, аргумента типа number .

Примечание

Ограничения позволяют сузить перечень типов, которые можно будет передать в обобщённом аргументе, и расширяет возможности по работе с этими типами.

А ещё пример выше может быть записан с использованием обощения Array , которое вскользь упоминалось во второй статье серии. В этом случае arg должен иметь все свойства и методы класса Array , включая необходимое в этом примере свойство .length :

function getLength(arg: Array): number

Если же в вашей функции требуется только свойство .length , то вы можете использовать встроенный обобщённый интерфейс ArrayLike :

function getLength(arg: ArrayLike): number

Можно ограничивать не только принимаемые аргументы по типу, но и сами параметр-типы. В большинстве руководств приводится пример копирования свойств объекта или получения значения свойства из объекта. Я предлагаю не отклоняться от них и рассмотреть работу функции обновления значения свойств объекта из другого объекта. Важным условием здесь является то, что мы можем лишь обновлять значения свойств, а не добавлять новые – да, тип пересечения ( Object.assign , который рассматривался в предыдущей статье серии) здесь не подойдёт.

function updateProperties(target: T, source: U): T < for (let key in target) < target[key] = source[key]; >return target; > 

Приведённая выше функция принимает на вход два типа: T и U , причём тип U расширяет тип T . На практике это означает, что в качестве аргумента source вы можете передать лишь тот объект, что включает в себя не только свои свойства, но и все обязательные свойства описанные типом T . Рассмотрим это утверждение на примере.

Пусть у нас есть три объекта, каждый из которых представлен своим интерфейсом, соответственно:

const a: IObjA = < a: 1 >; const b: IObjB = < b: 2 >; const c: IObjC = < a: 3, b: 3 >; 

Теперь попробуем передать в нашу функцию объект из переменной a и b :

updateProperties(a, b); // Error → Type "IObjB" does not satisfy the constraint "IObjA". Property "a" is missing in type "IObjB". 

Компилятор выдаст ошибку, потому что интерфейс IObjB не имеет свойства a , которое требуется интерфейсом IObjA . Такое поведение оправданно наличием ограничения. Однако, при передаче объекта, имплементирующего интерфейс IObjC , ошибки не будет, потому что интерфейс IObjC предполагает наличие обязательного свойства a .

Уточнения с помощью обобщений

Рассмотрим ситуацию, в которой у нас имеется функция, возвращающая значение свойства из переданного объекта по его имени. Даже в том случае, если мы напишем интерфейс и имплементируем его, компилятор не сможет понять: принадлежит ли переданное имя свойства переданному объекту или нет. И в этот момент на помощь приходит ещё одно ключевое слово keyof , предоставляющее доступ ко всем именам свойств в структурном типе данных.

Все мы знаем, что типов и поколений покемонов так много, что без TypeScript-а и структур данных здесь разобраться не получится. Давайте напишем простейший интерфейс, описывающий покемонов: тип, вес, высота и сила атаки.

enum PokemonType < Fire, Water, Flying >interface IPokemon

Хорошо, а теперь посмотрим на работу ключевого слова keyof – создадим некоторый тип K1 , включающий в себя имена всех свойств интерфейса IPokemon :

type K1 = keyof IPokemon; // type | weight | height | attack 

А теперь напишем функцию, которая умеет работать не только с интерфейсом покемонов, но и с другими интерфейсами, используя обобщения.

function getProperty(obj: T, key: K): T[K]

Такая функция, как правило, не требует явной передачи типов, так как компилятор сам в состоянии посмотреть на объект и применить его тип к типу T , попутно создав тип K , который является типом объединения для всех имён свойств этого объекта. Сложновато, но давайте попробуем.

const charmander: IPokemon = < type: PokemonType.Fire, weight: 8.5, height: 0.6, attack: 52 >; const type = getProperty(charmander, 'type'); // Type: PokemonType const health = getProperty(charmander, 'health'); // Error → Argument of type "health" is not assignable to parameter of type "type" | "weight" | "height" | "attack". 

В первом случае мы видим, что переменная type не только получила значение, хранящееся в свойстве, но и его тип – PokemonType . Во втором случае мы получили ошибку компилятора, потому что объект charmander не имеет свойства с именем health .

Замечательно, а что нам это даёт? Зачем все эти сложные конструкции с keyof , когда можно обратиться к свойству объекта через точку или квадратные скобки? – всё не так просто, как может показаться на первый взгляд.

Врядли кто-то из нас будет писать функцию, которая просто возвращает значение свойств без каких-либо ещё действий и логики. Конструкция keyof – это гарант того, что вы пытаетесь передать в функцию только разрешённый объект и существующее имя свойства, при этом взамен вы получаете не только значение свойства, но и его тип.

Для примера упростим функцию getProperty до примитивного состояния и попытаемся получить значение свойства type , просто передав его в качестве аргумента key :

function getProperty(obj: T, key: string) < return obj[key]; >const type = getProperty(charmander, 'type'); // Type: any const hp = getProperty(charmander, 'health'); // Type: any 

Ошибок нет. Типов нет. Уверенности в том, что функция не просто работает, а работает правильно – нет. Именно для этого нужна статическая типизация – для того, чтобы быть уверенным в том, что ваша функция работает так, как вы и задумывали. Здесь лишь одна оговорочка: речь идёт только про возвращаемый тип, а не про поведение функции.

Обобщения и интерфейсы

Обобщения не обошли стороной и интерфейсы.

interface User  < identificator: T; >type UserWithNumberIdentificator = User; type UserWithStringIdentificator = User; 

Но это скучно и понятно – давайте попробуем посмотреть на более сложный пример. Допустим, имеется интефейс, описывающий пользователя какого-нибудь простенького сервиса:

interface IPerson

Вы используете этот интерфейс в своих функциях и в какой-то момент вам понадобился точно такой же интерфейс, но свойства которого являются необязательными. И вот снова выбор: копировать и расставлять ? (необязательные параметры) или написать код, который может быть использован не один раз, то есть переиспользоваться. Конечно же вы выбрали второй путь, иначе самое время закрыть эту вкладку браузера.

Достичь универсальности поможет сопоставление и обобщения. В примере ниже я описал тип Partial , который возвращает новый тип, но делает обязательные свойства – необязательными. В этом типе используется индексируемый тип, раскрываемый для каждого имени свойства передаваемого объекта.

type Partial = < [K in keyof T]?: T[K]; >; type PartialPerson = Partial; // Type: < // name?: string; // age?: number; // money?: number; // >

Однако не спешите писать такой тип в ваших проектах сами: тип Partial уже существует в TypeScript и не требует пользовательского определения.

Некоторые встроенные типы для обобщений

В функциональном программировании, да и в программировании вообще, часто встречается необходимость изъять из какого-либо объекта не значение одного свойства, а двух или нескольких. Например, это может быть «фильтр» данных. Напишем такой «фильтр», используя уже знакомые нам типы и структуры, в рамках всё тех же покемонов.

function pick(obj: T, . keys: K[]) < const set: any = <>; for (const key in obj) < if ((keys).indexOf(key) !== -1) < set[key] = obj[key]; >> return set; > const a = pick(charmander, ['type', 'weight']); // Type: any 

Да, это не вызывает ошибки компиляции и мы до сих пор можем вписать в массив лишь имена существующих свойств, но компилятор думает, что функция возвращает любой тип и ничего не знает о нём. Именно для этого в TypeScript есть встроенный тип Pick .

function pick(obj: T, . keys: K[]): Pick  < const result = <>as Pick; for (const key in obj) < if ((keys).indexOf(key) !== -1) < result[key] = obj[key]; >> return result; > const a = pick(charmander, 'type', 'weight'); // Type: Pick

Примечание

Код немного грязный, но я не смог придумать более элегантного решения с типами.

Основные типы для обобщений:

  • Partial – указывает, что все свойства некоторого типа T являются необязательными
  • Readonly – указывает, что все свойства некоторого типа T доступны только для чтения
  • Record – конструирует объект, у которого значения свойств имеют некоторый тип T
  • Pick – выделяет из некоторого типа T некоторый набор свойств K

А вот пример с pluck , тип которого не предоставляется TypeScript из коробки. Эта функция принимает только объекты, у которых есть общие свойства и возвращает массив с их значениями. Если вы укажете b в качестве ключа, то компилятор выдаст ошибку.

function pluck(objs: T[], key: K): T[K][] < return objs.map(obj =>obj[key]); > const a = pluck([< a: 1 >, < a: 'c', b: 2 >], 'a'); // Type: (number|string)[] 

TypeScript имеет большое количество стандартных типов, которые могут использоваться в вашем коде с обобщениями. Полный список не описывается в документации, но вы можете почитать описание ES5. Также существует модуль typescript-collections, имплементирующий многие «стандартные» структуры данных на TypeScript.

Обобщения в классах

Для примера напишем класс очереди сразу применяя все знания полученные в этой статье. Пусть класс Queue принимает некоторый обобщённый тип T , а также имплементирует методы push и pop .

class Queue  < private data = []; public push = (item: T) =>this.data.push(item); public pop = (): T => this.data.shift(); > const queue = new Queue(); 

Здесь особо не о чем говорить, потому что всё то, что можно сделать с обобщениями в интерфейсах – можно сделать и в классах. Просто имейте в виду. Зато здесь можно поговорить об ограничениях.

В параметр-типе можно задавать не более одного ограничения конструктора – ограничение задаётся с помощью ключевого слова new , гарантирующего, что переданный аргумент вообще может быть инициализирован. Для примера напишем функцию createInstance , которая принимает какой-либо класс в качестве аргумента и возвращает экземпляр этого класса:

function createInstance(c: < new(): T; >): T < return new c(); >class Animal < numLegs: number; >const animal = createInstance(Animal); // Animal 

При необходимости можно ограничить принимаемое множество типов, если указать, что некоторый тип A должен быть потомком класса Animal , используя уже известный синтаксис – расширитель extends :

function createInstance(c: new() => A): A < return new c(); >class Animal < numLegs: number; >class Lion extends Animal <> const lion = createInstance(Lion); 

Если же передать класс, который наследуется не от класса Animal , то компилятор выдаст ошибку, говорящую о том, что мы пытаемся его обмануть и подсунуть неверный тип:

class Panda < awesome: boolean; >const panda = createInstance(Panda); // Error → Argument of type 'typeof Panda' is not assignable to parameter of type 'new () => Animal'. 

Ссылочки

По традиции оставляю пару ссылочек, которые помогут понять материал не только с моей стороны, но и со стороны других авторов. Не удивляйтесь, что в ссылках присутствуют материалы по C#, так как TypeScript берёт многие возможности именно из него – напомню, что у них один автор.

  • Документация по обобщениям
  • TypeScript Deep Dive – Generics
  • Leveraging the power of generics with TypeScript
  • From JavaScript to TypeScript, Pt. IV: Generics & Algebraic Data Types
  • Generics in C#
  • C# Generic Class
  • Using Generics With C#

Делимся на оплату хостинга или кофе.
Чем чаще пью кофе, тем чаще пишу статьи.

Saved searches

Use saved searches to filter your results more quickly

Cancel Create saved search

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.

Список вопросов по TypeScript для подготовки к собеседованию. Основы TypeScript.

FedorovAlexander/typescript-interview-questions-ru

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Switch branches/tags
Branches Tags
Could not load branches
Nothing to show
Could not load tags
Nothing to show

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?

Cancel Create

  • Local
  • Codespaces

HTTPS GitHub CLI
Use Git or checkout with SVN using the web URL.
Work fast with our official CLI. Learn more about the CLI.

Sign In Required

Please sign in to use Codespaces.

Launching GitHub Desktop

If nothing happens, download GitHub Desktop and try again.

Launching GitHub Desktop

If nothing happens, download GitHub Desktop and try again.

Launching Xcode

If nothing happens, download Xcode and try again.

Launching Visual Studio Code

Your codespace will open once ready.

There was a problem preparing your codespace, please try again.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *