Содержание
- Что такое TypeScript? Для чего он нужен и какие проблемы решает?
- Рассказать про следующие виды типизации, их особенности, плюсы и минусы
- Какие виды типизации используются в JavaScript и TypeScript?
- Как и для каких целей используются типы данных, приведенные ниже?
- Для чего используются type assertions? Почему стоит их избегать?
- Для чего предназначены интерфейсы?
- Что такое Type Aliases? Зачем нужны? Чем отличаются от интерфейсов?
- Что делает ? после имени свойства интерфейса или аргумента функции?
- Зачем нужен постфикс !? Почему его стоит избегать?
- От чего могут наследоваться интерфейсы? Какие накладывает ограничения наследование от класса?
- Какие есть способы типизации static-side класса и instance-side класса?
- Может ли класс одновременно реализовывать набор интерфейсов и наследоваться от другого класса?
- Каковы назначения модификаторов свойств классов?
- В чем отличие абстрактных классов от обычных классов и интерфейсов?
- Можно ли объявить в интерфейсе тип метода таким образом, чтобы он возвращал контекст своего вызова?
- Для чего используются Hybrid Types?
- Для чего используются Index types?
- Для чего предназначены Mapped types? Как создать собственный Mapped type?
- Для чего нужны generics
- Union и intersection типы
- Что такое Type Guards? Для чего они нужны?
- Как работают основные Type Guards:
- Как объявить свой Type Guard?
- Что такое Assertion Functions и для чего они нужны?
- Что такое Discriminated union?
- Для чего предназначены Conditional Types?
TS
Что такое TypeScript? Для чего он нужен и какие проблемы решает?
TypeScript — это язык программирования со статической типизацией, позиционирующий себя как язык, расширяющий возможности JavaScript.
- Какие проблемы решает:
- Выявление ошибок на этапе компиляции, а не на этапе выполнения
- За счет системы типов разработчики получают такие возможности, как подсказки и переходы по коду, которые значительно ускоряют процесс разработки
- Система типов в значительной степени избавляет разработчиков от комментирования кода, которое отнимает значительное время
- Система типов помогает раньше выявлять проблемы архитектуры, исправление которых обходятся дешевле на ранних этапах
Рассказать про следующие виды типизации, их особенности, плюсы и минусы
Слабая / сильная
Сильная типизация: НЕ разрешает выполнение выражений с несовместимыми типами и не выполняют неявное преобразование типов в ситуациях, когда нужно выполнять преобразование явно.
- Плюсы:
- при операциях способных привести к потере или порче значения возникает ошибка. Благодаря этому сильно типизированные языки в меньшей степени подвержены багам.
Слабая типизация: разрешают выполнение выражений с любыми типами и самостоятельно выполняют неявное преобразование
- Минусы:
- Из-за того, что в языках со слабой типизацией допускаются неявные преобразования существует высокая вероятность возникновения трудно выявляемых багов. Причиной появления подобных багов является отсутствие возникновения ошибок в некоторых операциях неявного преобразования. Программа получает данные ожидаемого типа, но непредсказуемого значения
Статическая / динамическая
Статическая типизация: контроль типов происходит на этапе компиляции.
Особенности. До того момента, как программа будет запущена, компилятор осуществляет проверку совместимости типов, участвующих в различных операциях. Все конструкции, нуждающиеся в аннотации типа, связываются с типом данных при объявлении. При этом связь с типом данных в дальнейшем не может быть изменена
- Плюсы:
- За счет того, что большая часть проверок происходит на этапе компиляции, программа обладает большей производительностью из-за отсутствия проверок во время выполнения
- Языки со статической проверкой типов значительно повышают семантическую привлекательность кода делая его более читабельным
- Благодаря статическому контролю типов, редактор кода способен на основе синтаксического анализа выводить вспомогательную информацию, ускоряющую разработку
Динамическая типизация: контроль типов происходит на этапе выполнения программы.
Особенности: конструкция нуждающаяся в аннотации типа, связывается с типом данных на этапе выполнения программы в момент присвоения значения. При этом связь с типом данных может быть изменена. Тип данных может быть определен только на основании вычисленного результата выражения. При этом операция присвоения значения может изменить тип данных сущности с которым она была связана ранее. Динамический контроль типов проверяет операнды на совместимость типов и совместимость с оператором в каждой отдельной операции.
- Плюсы:
- Минусы:
- Проверка типов данных во время выполнения программы отрицательно сказывается на её производительности. Поэтому языки с динамическим контролем типов проигрывают в производительности языкам со статическим контролем типов у которых совместимость проверяется на этапе компиляции.
- По причине выявления несовместимости типов только во время выполнения программы ошибки выявляются менее эффективно. Программа не узнает о скрывающейся в ней ошибке, пока не дойдет очередь выполнения проблемного участка кода.
- Редакторы кода, даже современные, испытывают трудности при синтаксическом анализе кода
явная/неявная
Явная типизация: типы языковых элементов (сигнатура функций и т.д.) явно прописывается в коде.
- Плюсы:
- делает код более понятным (читабельным)
- способствует развитию у разработчика навыков необходимых для проектирования архитектуры программ
Неявная типизация: типы языковых элементов не прописывается в коде
- Плюсы:
- более лаконичный код
- Минусы:
- труднее разобраться в коде
Структурная / номинативная
Структурная типизация: определяет совместимость типов основываясь не на иерархии наследования или явной реализации интерфейсов, а на их описании. Компилятор считает типы совместимыми, если сопоставляемый тип имеет все признаки типа, с которым сопоставляется. Чтобы быть совместимым, сопоставляемый тип должен иметь те же ключи с теми же (идентичными или совместимыми) типами, что и тип, с которым происходит сопоставление
Номинативная типизация: устанавливает совместимость типов основываясь на идентификаторах типов (ссылках). При проверке на совместимость компилятор проверяет иерархию типов на признаки наследования и реализацию интерфейсов. То есть, тип B будет совместим с типом A только тогда, когда он является его предком (extends). Кроме того, тип B будет совместим с интерфейсом IA только в том случае, если он или один из его предков реализует его явно (implements)
Как работает структурная типизация в Typescript:
class Car {
id: number;
numberOfWheels: number;
move (x: number, y: number) {
// некая реализация
}
}
class Boat {
id: number;
move (x: number, y: number) {
// некая реализация
}
}
let car: Car = new Boat(); // здесь TypeScript выдаст ошибку, в Boat отсутствует свойство numberOfWheels
let boat: Boat = new Car(); // а на этой строчке все в порядке - Класс Car содержит все свойства и методы класса Boat, поэтому Car может использоваться в качестве Boat
Какие виды типизации используются в JavaScript и TypeScript?
JavaScript: Динамическая | Слабая | Неявная
TypeScript: Статическая | Сильная | Явная | Структурная
Как и для каких целей используются типы данных, приведенные ниже?
Boolean
boolean - примитивные значения логических литералов (логический тип, представлен значениями "истина" true и "ложь" false.)
Boolean - конструктор типа boolean
Когда применять boolean: для хранения логических значений
Number
number - примитивные значения числовых литералов (числа с плавающей запятой от -(2 53-1) до (253-1), Infinity, -Infinity и NaN)
Number - конструктор типа number
Когда применять number: для хранения числовых данных
String
string - примитивные значения строковых литералов (последовательность символов в кодировке Unicode UTF-16. Строки должны быть заключены в одинарные или двойные кавычки, а также в обратные апострофы (инициаторы так называемых шаблонных строк))
String - конструктор типа string
Когда применять string: для хранения строковых данных
Null and Undefined
Примитивный тип Null служит обозначением “ничего”. (не путать с единственным литеральным значением null типа Null, которое присваивается в качестве значения)
Примитивный тип undefined указывает на то, что значение не определено. Тип данных undefined указывается с помощью ключевого слова undefined (не путать со свойством глобального объекта undefined, которое представляет единственное значение типа Undefined).
let identifier: undefined = undefined; /* undefined, указанный после оператора двоеточия, это имеющийся только в
TypeScript псевдоним (alias) для глобального типа Undefined. В, то время как undefined, указанный после оператора
присваивания, это единственное значение типа Undefined.*/
Во время выполнения объявленные, но не инициализированные переменные, поля и свойства класса, а также параметры имеют значение undefined. Также значение undefined является результатом вызова методов или функций, которые не возвращают значения.
Когда применять null и undefined: Обычно null используется для присвоения переменной «пустого» или «неизвестного» значения, а undefined – для проверок, была ли переменная назначена.
Object and object
object - описывает любой непримитивный тип
Typescript определяет другой тип, с почти таким же названием, как и новый тип object, и это тип Object. В то время как object (с маленькой буквы) представляет все непримитивные типы, Object (с большой буквы) описывает функциональность, общую для все JavaScript объектов. Переменным, которым указан тип с помощью ключевого слова object, не могут хранить значения примитивных типов, чьи имена начинаются со строчной буквы. В отличии от них, тип интерфейс Object, совместим с любым типом данных.
let elephant: object;
let lion: Object;
elephant = 5; // Error
lion = 5; // Ok
Есть ещё один тип, который очень похож: {}, пустой тип объекта. Он описывает объект у которого нет собственных свойств.
Когда применять Object:
Когда применять object:
Array
Тип массива может быть определен одним из двух способов. Первый - обозначать тип элементов массива перед []:
let list: number[] = [1, 2, 3];
Второй способ - использовать обобщение Array
let list: Array<number> = [1, 2, 3];
Когда применять Array: для хранения упорядоченных коллекций данных, пронумерованных начиная с нуля
Tuple
Тип Tuple дает вам возможность объявить массив с известным фиксированным количеством элементов, которые не обязаны быть одного типа. Например, вы хотите иметь значение Tuple как пару "строка" и "число":
// Объявление типа tuple
let x: [string, number];
// Его инициализация
x = ['hello', 10]; // OK
// Некорректная инициализация вызовет ошибку
x = [10, 'hello']; // Error
Когда применять tuple: когда нужно объявить массив с известным фиксированным количеством элементов определенных типов
Enum
Enum — это конструкция, состоящая из набора именованных констант (список перечисления) и определяемая такими примитивными типами, как number и string. Enum объявляется с помощью ключевого слова enum
Enum это удобный способ задания понятных имен набору численных значений
enum Color {Red, Green, Blue};
let c: Color = Color.Green;
По умолчанию перечисления (Enum) начинаются с 0. Вы можете изменить это путем прямого указания значения для одного из членов перечисления. Например, мы можем начать предыдущий пример с 1 вместо 0:
enum Color {Red = 1, Green, Blue};
let c: Color = Color.Green;
Или даже задать значения для всех членов:
enum Color {Red = 1, Green = 2, Blue = 4};
let c: Color = Color.Green;
Удобная особенность перечислений состоит в том, что вы также можете получить имя члена перечисления, передав его числовое значение. Например, если у нас есть значение 2 и мы хотим посмотреть, чему оно соответствует в перечислении Color описанном выше, мы можем сделать это так:
enum Color {Red = 1, Green, Blue};
let colorName: string = Color[2];
Когда применять enum:
1) когда нужна двухсторонняя ассоциация строкового ключа с его числовым или строковым значением (проще говоря, карта строковый ключ — числовое значение_числовой ключ — строковое значение_).
2) enum лучше всего подходит для определения дискриминантных полей
Any
Тип any (любой) — это тип в TypeScript, подходящий к чему угодно. Поскольку он охватывает типы всех возможных значений, то не заставляет нас делать какие-либо проверки, прежде чем мы попытаемся эти значения вызывать, конструировать или получать доступ к их свойствам. Он также позволяет присвоить значения типа any переменным, которые ожидают значений любого другого типа. Эта возможность в целом полезна, но не может обеспечить достаточную строгость
Все типы в TypeScript являются его подтипами. Это означает, что он совместим в обе стороны с любым другим типом и с точки зрения системы типов является высшим типом (top type). Значение принадлежащие к типу any совместимо с любыми другими значениями
Когда применять any: используетя, если тип конструкций может быть неизвестен. Примером этого может служить сервис, который работает с сервером посредством api. Полученные и сериализованные данные могут храниться как тип any прежде чем они будут и преобразованы к конкретному типу.
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // ok, это определенно boolean
Void
Тип данных Void можно назвать полной противоположностью типа any, так как этот тип означает отсутствие конкретного типа. Основное предназначение типа Void — явно указывать на то, что у функции или метода отсутствует возвращаемое значение. Тип данных Void указывается с помощью ключевого слова void (не путать с одноимённым оператором из JavaScript) и, в отличие от таких типов, как null и undefined, не имеет никаких значений.
Когда применять void: чаще всего он используется в качестве возвращаемого типа функций, которые не возвращают никакого значения.
function warnUser(): void {
alert("This is my warning message");
}
Never
это примитивный тип, который олицетворяет собой признак для значений, которых никогда не будет. Или, признак для функций, которые никогда не вернут значения, то ли по причине ее зацикленности, например, бесконечный цикл, то ли по причине ее прерывания.
/** Пример с прерыванием */
function error(message: string): never {
throw new Error(message);
}
/** Бесконечный цикл */
function infiniteLoop(): never {
while (true) {
}
}
/** Божественная рекурсия */
function infiniteRec(): never {
return infiniteRec();
}
Когда применять never:
Unknown
Иногда в TypeScript хочется описать тип, не подходящий ни к чему. Это бывает полезно для API, который хочет просигнализировать: «здесь может быть любое значение, поэтому вы должны произвести некоторую проверку перед тем, как его использовать». И пользователи вынуждены анализировать возвращаемые значения в целях безопасности. В TypeScript 3.0 вводится новый тип с названием unknown, который делает именно это. Подобно типу any, типу unknown присваивается любое значение, однако, в отличие от any, тип unknown не может быть присвоен почти никакому другому без утверждения типа. Вы не можете получать доступ к объектам типа unknown, а также вызывать их или конструировать.
Когда применять unknown:
Для чего используются type assertions? Почему стоит их избегать?
Type assertions - утверждение типа, возникает в случае, когда фактический тип некоторой сущности является более специфичным, чем ее текущий. В TypeScript большинство операций с несоответствием типов приходится на работу с dom.
Например, метод querySelector(): результатом его вызова может стать любой элемент, находящийся в dom-дереве. Если бы в качестве типа возвращаемого значения был указан тип HTMLElement, то операция получения элемента <script> или <link> завершилась бы неудачей, так как они не принадлежат к этому типу. Именно поэтому методу querySelector() в качестве типа возвращаемого значения указан более базовый тип Element
Но, при попытке обратиться к свойству dataset через объект, полученный с помощью querySelector(), возникнет ошибка, так как у типа Element отсутствует данное свойство. Факт, что разработчику известен тип, к которому принадлежит объект по указанному им селектору, дает ему основания попросить вывод типов пересмотреть свое отношение к типу конкретного объекта.
Если тип, к которому разработчик просит преобразовать компилятор, не совместим с преобразуемым типом, то в процессе утверждения возникнет ошибка.
//синтаксис угловых скобок
const element: Element = document.querySelector('#stage');
const stage: HTMLElement = element // Ok
stage.dataset.inactive = 'true';
// синтаксис as
let element = document.querySelector('#counter') as HTMLElement;
выражения, требующие утверждения типа, при работе с dom api — это неизбежность. Но в случае, если утверждение требуется для кода, написанного самим разработчиком, то, скорее всего, это следствие плохо продуманной архитектуры. Поэтому необходимо тщательнее прорабатывать архитектуру, вместо того чтобы злоупотреблять утверждением типов там, где этого можно избежать
Для чего предназначены интерфейсы?
Interface — это синтаксическая конструкция, предназначенная для описания открытой (public) части объекта, класса, функционального выражения без реализации (api), т.е. описания состояния и поведения (какие свойства и методы он содержит, какие типы они возвращают и т.д.).
Интерфейс объявляется с помощью ключевого слова interface, после которого указывается идентификатор (имя), за которым следует заключенное в фигурные скобки тело содержащее описание.
interface Identifier {
// тело интерфейса
}
Что такое Type Aliases? Зачем нужны? Чем отличаются от интерфейсов?
Псевдоним типа объявляется при помощи ключевого слова type, после которого следует идентификатор (имя) псевдонима, а за ним идет оператор присваивания =, справа от которого находится тип, ассоциирующийся с псевдонимом. Например:
class Type {
method(): void {
type Alias = Type;
}
}
type Alias = Type;
Разберем разницу между интерфейсом и типом в TS. С выходом новых версий TS отличия между ними очень сильно размываются и уже давно тип и интерфейс являются взаимозаменяемыми почти во всех случаях. Интерфейс и тип мы можем использовать для описания свойств объекта:
interface UserA {
name: string;
age: number
}
type UserB = {
name: string;
age: number
}
const user1: UserA = {
name: 'Ivan',
age: 20
}
const user2: UserB = {
name: 'Petr',
age: 30
}
В данном случае они будут полностью взаимозаменяемы. Разница только в синтаксисе. Интерфейс объявляется в синтаксисе похожем на класс, а тип - в синтаксисе похожем на объект.
Также и тип и интерфейс можно использовать для описания поведения функции. Разницы никакой нет, сигнатура осталась той же:
interface SumA {
(a: number, b: number): number
}
type SumB = (a: number, b: number) => number;
const sum1: SumA = (a, b) => a + b;
const sum2: SumB = (a, b) => a + b;
В старых версиях TS нельзя было наследоваться интерфейсом от типа, но в последних версиях это возможно (если это объектный тип (object type) или пересечение объектных типов со статически известными членами):
type B = {
name: string;
}
interface A extends B {} // интерфейс наследуется от типа
const B: A = {
name: 'Lena';
}
Как видим, в большинстве случаев интерфейсы и типы являются взаимозаменяемыми
Различия:
1) с помощью типа можно сделать alias для какого-то примитивного значения:
type UniqueID = string;
Далее чтобы писать более понятные типы, его можно использовать в других интерфейсах для большего понимания, что происходит в системе:
interface User {
id: UniqueID
}
Alias примитива можно сделать только с помощью типа. Но если это alias не для примитивного значения, а например для массива, то можно использовать и интерфейс
type NamesA = string[];
interface NamesB { /* Здесь определен интерфейс, который содержит сигнатуру массива. Эта сигнатура указывает,
что объект, который реализует NamesB, может индексироваться с помощью чисел (объекта типа number). И, кроме того,
данный объект должен хранить объекты типа string, то есть строки.*/
[key]: number: string
}
2) с помощью интерфейса невозможно описать тип кортежа (кортеж это массив, длина которого не изменяется)
type State = [number, (a: number) => void]
const tup = [123, (n) => {}]
3) мы можем создать тип, который будет частично имплементировать другой тип
type UserA = {
name: string;
}
type UserB = {
age: number
}
type C = A | B;
const obj1: C = { // ok
name: 'S',
}
const obj2: C = { // ok
age: 10,
}
const obj3: C = { // надо проверить, может ли объект содержатьоба свойства
name: 'S',
age: 10,
}
Написать подобный интерфейс невозможно
interface D extends A | B {} // невалидный код
interface D extends C {} // невалидный код, т.к. экстендиться от типа, который содержит внутри себя union, невозможно
interface D extends A, B {} // ok, но тогда обязательными будут оба поля
4) Типы, в отличие от интерфейсов, не могут быть объявлены несколько раз
type A = {
age: number
}
type A = { // ошибка
name: string
}
interface B = {
age: number
}
interface B = { // ok, произошел automerge, расширение интерфейса. Теперь объект с интерфейсом B должен иметь два поля
name: string
}
const obj: B = {
age: 20,
name: 'Masha'
}
5) Псевдонимы типов нельзя использовать в таких операциях с типами времени выполнения как typeof и instanceof - ??????? ЧТО ЭТО ЗНАЧИТ?????
Что делает ? после имени свойства интерфейса или аргумента функции?
Оператор Optional - делает это свойство или аргумент необязательными
В чем различие между x?: number и x: number | undefined?
x?: number - означает, что свойство х является необязательным, но если оно есть, то должно иметь тип number
x: number | undefined? - означает, что свойство х является обязательным и должно имет тип number или undefined
Зачем нужен постфикс !? Почему его стоит избегать?
Non-Null and Non-Undefined Operator - оператор ненулевого утверждения (постфикс !). Используется, когда происходит обращение к полям и свойствам объекта, помеченного как необязательный.
function duplicate(text?: string) {
let fixString = function() {
if (text === undefined) {
text = "";
}
};
fixString();
return text.concat(text); // ошибка Object is possibly 'undefined'
}
/* Поскольку аргумент text помечен как необязательный, вывод типов определяет его тип как string|undefined,
в связи с чем не может вызвать на нем метод concat (т.к. он не может быть вызван на undefined) и выбрасывает
ошибку Object is possibly 'undefined'. То, что проверка на undefined производится в функции fixString и если text =
undefined, то он приравнивается к пустой строке (т.е. в итоге text всегда будет определен) - это TS не может проанализировать */
Это тот случай, когда разработчик знает о коде больше, чем TypeScript. Стоит отметить, что если бы код был более
простым, то TypeScript понял бы, что text не может быть равен нулю:
function duplicate(text: string | null) {
if (text === null || text === undefined) {
text = "";
}
return text.concat(text);
}
Добавим оператор ненулевого утверждения:
function duplicate(text: string | null) {
if (text === null || text === undefined) {
text = "";
}
return text!.concat(text!);
}
Ошибка исчезла. Мы можем использовать это только тогда, когда мы точно знаем, что переменная или выражение не
могут быть nullили undefined. Обычно в таких случаях стоит изменить архитектуру, но если разработчик в полной мере
осознает последствия, то компилятор можно настоятельно попросить закрыть глаза на потенциально опасное место при
помощи оператора Not-Null Not-Undefined.
Почему его стоит избегать? Потому что обычно необходимость использования этого оператора вызвана некачественной архитектурой, соответсвенно, в хорошо продуманной архитектуре необходимости в нем не возникло бы
Для чего используется с полями классов? Например:
class C {
foo!: number;
...
полю foo назначен тип number, но в нестрогом режиме (флаг strictNullChecks не установлен) значения null и undefined могут быть присвоены переменной любого типа. В этом случае, если поле foo участвует в таких операций, где значение null или undefined не допустимо, TS выбросит ошибку. Если же разработчик точно знает, что это поле никогда не примет значение null или undefined, то он может избавиться от данной ошибки используя оператор ненулевого утверждения.
От чего могут наследоваться интерфейсы? Какие накладывает ограничения наследование от класса?
Наследование интерфейсов осуществляется с помощью ключевого слова extends, после которого через запятую идет один или несколько идентификаторов расширяемых интерфейсов / типов / классов
От других интерфейсов
interface IIdentifiable {}
interface ILiving {}
// интерфейсы IIdentifiable и ILiving вместе образуют логически связанную композицию,
// которую можно выделить в интерфейс IAnimal
interface IAnimal extends IIdentifiable, ILiving {}
От типов
В старых версиях TS нельзя было наследоваться интерфейсом от типа, но в последних версиях это возможно (если это объектный тип (object type) или пересечение объектных типов со статически известными членами):
type B = {
name: string;
}
interface A extends B {} // интерфейс наследуется от типа
const B: A = {
name: 'Lena';
}
От классов
class Animal {
nickname: string;
age: number;
}
interface IAnimal extends Animal {} //интерфейс наследуется от класса
class Bird implements IAnimal {
nickname:string;
age: number;
}
let bird: IAnimal = new Bird();
Интерфейс, полученный путем расширения типа класса, может быть реализован только самим этим классом или его потомками, поскольку помимо публичных (public) также наследует закрытые (private) и защищенные (protected) члены.
class Animal {
private uid: string;
protected maxAge: number;
public name: string;
}
interface IAnimal extends Animal {}
class Bird extends Animal implements IAnimal { // Ok
// private uid: string = ''; // Error, private
protected maxAge: number = 100; // Ok, protected
public name: string = 'bird'; // Ok, public
}
class Fish implements IAnimal { // Error
public name: string = 'fish';
}
let bird: IAnimal = new Bird(); // Ok
let fish: IAnimal = new Fish(); // Error
Какие есть способы типизации static-side класса и instance-side класса?
Сначала нужно определиться с терминологией.
Когда мы говорим о классе, мы можем говорить о его статических аспектах, которые относятся к конструктору (обычно в единственном числе, «конструктор», потому что есть только один из них, который одинаков для всех экземпляров), или речь идет об экземплярах класса (обычно во множественном числе, «экземпляры», потому что один конструктор может создавать множество экземпляров, каждый из которых может иметь состояние или свойства от других, отличные от других).
С точки зрения системы типов тип Foo соответствует типу экземпляра (instance side). Чтобы получить статический тип (static side), мы можем использовать оператор запроса типа typeof, работающий со значением конструктора с именем Foo: typeof Foo. Таким образом, Foo — это тип экземпляра, а typeof Foo — тип статической стороны (??тип класса??).
class Foo {
instanceProp: string;
constructor(constructorArgument: string) {
this.instanceProp = constructorArgument;
}
instanceMethod(): void {
console.log("Instance method called on " + this.instanceProp)
}
static staticProp: string = "Static Thing";
static staticMethod(): void {
console.log("Static method called on " + this.staticProp)
}
}
- Это единое определение класса, но оно имеет статическую сторону и сторону экземпляра. Статическая сторона
Foo, то есть вещи, относящиеся к его конструктору, включают в себя:
- собственно конструктор, сигнатурой которого является {new(constructorArgument: string): Foo}
- свойство staticProp, типом которого является string
- метод staticMethod, сигнатурой которого является {(): void}
interface StaticSideOfFoo {
new(constructorArgument: string): Foo;
staticProp: string;
staticMethod(): void;
}
- Тип объекта включает в себя
- свойство instanceProp типа string
- метод instanceMethod с сигнатурой {(): void}
Этот тип называется Foo, и вы также можете определить для него свой собственный интерфейс:
interface InstanceSideOfFoo {
instanceProp: string;
instanceMethod(): void;
}
Убедимся, что мы понимаем разницу
const foo1 = new Foo("Number 1");
console.log(foo1.instanceProp); // Number 1
foo1.instanceMethod(); // метод экземпляра вызван на Number 1
const foo2 = new Foo("Number 2");
console.log(foo2.instanceProp); // Number 2
foo2.instanceMethod(); // метод экземпляра вызван на Number 2
console.log(Foo.staticProp); // Static Thing
Foo.staticMethod(); // статический метод вызван на классе
Обратите внимание, что foo1 и foo2 не имеют прямого доступа к сигнатуре конструктора или к staticProp или
staticMethod:
new foo1("oops"); // error!
foo1.staticProp; // error!
foo1.staticMethod(); // error!
и что Foo не имеет прямого доступа к instanceProp или instanceMethod:
Foo.instanceProp; // error!
Foo.instanceMethod(); // error!
И значения foo1 и foo2 имеют тип Foo, аналогичный InstanceSideOfFoo, тогда как значение Foo имеет тип typeof Foo,
аналогичный StaticSideOfFoo. Мы можем убедиться в этом здесь:
const instanceSideOfFoo: InstanceSideOfFoo = foo1; // okay
const instanceSideOfFooOops: InstanceSideOfFoo = Foo; // error!
const staticSideOfFoo: StaticSideOfFoo = Foo; // okay
const staticSideOfFooOops: StaticSideOfFoo = foo1; // error!
Дополнительно про static-side и instance-side Здесь
Еще одно объяснение
Предположим, у нас есть код:
class MyClass {
someMethod() { }
}
let x: MyClass;
x = MyClass; // ошибка: Свойство "someMethod" отсутствует в типе "typeof MyClass" и является обязательным в типе "MyClass"
Важно помнить, что в JavaScript классы — это просто функции. Мы ссылаемся на сам объект класса — значение MyClass
— как на функцию-конструктор. Когда функция-конструктор вызывается с помощью new, мы возвращаем объект, который
является экземпляром класса.
Поэтому, когда мы определяем класс, мы фактически определяем два разных типа.
Первый — это тот, на который ссылается имя класса; в данном случае MyClass. Это тип экземпляра класса . Он определяет свойства и методы, которыми обладает экземпляр класса. Это тип, возвращаемый вызовом конструктора класса.
Второй тип анонимный. Это тип, который имеет функция-конструктор. Он содержит сигнатуру конструкции (возможность вызова с помощью new), которая возвращает экземпляр класса. Он также содержит любые статические свойства и методы, которые могут быть у класса. Этот тип обычно называют «статической стороной» класса , поскольку он содержит эти статические члены (а также является конструктором для класса). Мы можем обратиться к этому типу с помощью оператора запроса типа typeof.
Оператор typeof (при использовании в позиции типа) выражает тип выражения. Таким образом, typeof MyClass относится к типу выражения MyClass — функции-конструктора, создающей экземпляры MyClass.
- Способы типизации
-
- static side
- с помощью запроса типа typeof [имя класса] - т.е. получаем тип ранее созданного класса
const statFoo1: typeof Foo = Foo; - описать конструктор и статические методы и свойства в интерфейсе:
interface StaticSideOfFoo { new(constructorArgument: string): Foo; staticProp: string; staticMethod(): void; }const statFoo2: typeof Foo = Foo;
- с помощью запроса типа typeof [имя класса] - т.е. получаем тип ранее созданного класса
-
- instance side
- Указав название класса
const instFoo1: Foo = new Foo("Number 1"); - описать методы и свойства (не статические, т.е. те, которые будут принадлежать объекту) в интерфейсе:
interface InstanceSideOfFoo { instanceProp: string; instanceMethod(): void; }const instFoo2: InstanceSideOfFoo = new Foo("Number 2");
- Указав название класса
Может ли класс одновременно реализовывать набор интерфейсов и наследоваться от другого класса?
Может:
interface IAnimal {}
interface IOviparous {}
class Bird implements IAnimal, IOviparous {}
interface IFlyable {}
interface IPredatory {}
class Eagle extends Bird implements IFlyable, IPredatory {} //надо проверить, может ли класс, наследующийся от другого класса, реализовывать более одного интерфейса (наверное может)
Каковы назначения модификаторов свойств классов?
Модификаторы доступа — это ключевые слова, с помощью которых осуществляется управление сокрытием данных в классе. Простыми словами, модификаторы доступа задают уровень доступности членам класса ограничивая область их видимости.
Чем меньше окружающему миру известно о внутреннем устройстве классов, тем меньше оно взаимодействует с ним. Взаимодействия программы с объектом называют сопряжением (coupling), уровень которого варьируется от сильного до слабого. Слабое сопряжение (loose coupling) является признаком качественной архитектуры программы. Этот факт подталкивает скрывать как можно больше информации при проектировании программы.
Модификаторы доступа и конструкторы класса
Конструктор, который не отмечен ни одним модификатором доступа эквивалентен конструктору, помеченному как public. Если класс состоит только из статических свойств и методов, как например класс Math, то его конструктор более разумно пометить как приватный и, тем самым, запретить создание его экземпляров. Кроме того, класс, у которого конструктор объявлен с модификатором доступа private, нельзя расширять (extends).
Класс, в котором конструктор объявлен с модификатором доступа protected, как и в случае с модификатором доступа private, также не позволит создать свой экземпляр, но открыт для расширения (extends).
Тем не менее есть один нюанс. Не получится создать экземпляр подкласса, если он не переопределил конструктор суперкласса.
Public. Члены, помеченные ключевым словом public (или не помеченные никаким - в этом случае они считаются public), доступны в определяющих их классах, их потомках, а также к ним можно обращаться через экземпляр или, в случае статических членов, через ссылку на класс.
Private является полной противоположностью модификатору доступа public. Члены, помеченные с помощью ключевого слова private, доступны только контексту класса, в котором они определены.
Protected. Члены, которым установлен модификатор доступа protected, доступны только контексту класса, в котором они определенны, а также всем его потомкам. Попытка обратится к членам, помеченным как protected снаружи, приведет к возникновению ошибки. Как правило, при современной разработке упор делается только на два вида модификаторов доступа - public и private. Но, в редких случаях, по сценарию может возникнуть необходимость обращаться к членам своего суперкласса в подклассах, которые при этом, должны быть скрыты от окружающего мира. В таких случаях члены помечают как protected.
В чем отличие private от #?
Влияние модификаторов доступа public, protected и private ограничивается TypeScript и после компиляции от них не остается ни следа. В скомпилированном коде нет никакой разницы между членами, к которым были применены те или иные модификаторы доступа
Для того, что бы поля класса оставались закрытыми после компиляции в JavaScript, необходимо прибегнуть к модификатору, определенному спецификацией ECMAScript # который указывается в качестве префикса их идентификаторов (имен) #identifier.
Доступ к защищенному полю класса ограничивается областью видимости класса в котором оно объявлено, а при обращении к нему необходимо также указывать символ решетки.
Поскольку доступ ограничивается областью видимости класса, классы-потомки не могут обращаться к защищенным полям своих предков.
Данный модификатор может быть применён не только ко всем членам экземпляра класса предусмотренного спецификацией ECMAScript, но и к членам самого класса (static). Модификация статических полей, при активном флаге возможна только при активации флаге --useDefineForClassFields.
В отличие от модификатора доступа private, этот механизм не может быть применён к методам и аксессорам класса, но, так как за его появлением стоит спецификация ECMAScript, он продолжает действовать в скомпилированной программе. Именно поэтому, в отличие от сценария с модификатором доступа private, потомки могут без страха нарушить ожидаемый ход выполнения программы и объявлять защищенные поля, чьи идентификаторы идентичны объявлениям в их супер-классах.
Нюансы:
1) Закрытые поля нельзя объявлять непосредственно в конструкторе.
2) Код, содержащий закрытые поля класса, может быть скомпилирован исключительно в версии es6 и выше.
В чем отличие абстрактных классов от обычных классов и интерфейсов?
Абстрактные классы могут содержать абстрактные члены, принадлежность к которым указывается с помощью ключевого слова abstract. Ключевое слово abstract можно применить к полям, свойствам (аксессоры) и методам абстрактного класса; абстрактные поля могут иметь значение ( но в этом случае обратится к нему могут только абстрактные классы в иерархии наследования); абстрактные свойства и методы не должны иметь реализацию. Кроме абстрактных членов, абстрактные классы могут содержать обычные члены, обращение к которым ничем не отличается от членов, объявленных в обычных классах. Как правило, абстрактные классы реализуют только ту логику, которая не будет ни при каких обстоятельствах противоречить логике своих подклассов.
Отличия от обычных классов:
1) Нельзя создать экземпляр абстрактного класса.
2) Абстрактный класс, расширяющий другой абстрактный класс, не обязан переопределять все абстрактные члены своего суперкласса. В отличие от абстрактных классов, обычные классы, расширяющие абстрактные классы, обязаны переопределить все абстрактные поля, свойства и методы, находящиеся в иерархической цепочке, если они не были реализованы предками ранее.
Отличия от интерфейсов:
Интерфейсы предназначены для описания публичного api, которое служит для сопряжения с программой. Кроме того, они не должны, а в TypeScript и не могут, реализовывать бизнес логику той части, которую представляют. Абстрактные классы, при необходимости, должны реализовывать интерфейсы в той же степени и для тех же целей, что и обычные классы. Их однозначно нужно использовать в качестве базового типа тогда, когда множество логически связанных классов имеет общую для всех логику, использование которой в чистом виде не имеет смысла. Другими словами, если логика размещенная в классе не может или не должна выполнятся отдельно от потомков, то необходимо запретить создание экземпляров подобных классов.
Подробнее про абстрактные классы и интерфейсыМожно ли объявить в интерфейсе тип метода таким образом, чтобы он возвращал контекст своего вызова?
interface ISomething {
getThis(): // указать тип
}
class Something implements ISomething {
public getThis() {
return this;
}
}
Ответ:
interface ISomething {
getThis(): ISomething;
}
const inst = new Something();
const call = inst.getThis(); // const call: Something
call имеет тип экземпляра класса (т.е. объекта, в контексте которого вызван метод getThis() )
Для чего используются Hybrid Types?
Гибридные типы используются для описания объекта, который может быть как функцией, так и объектом, с дополнительными свойствами
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Для чего используются Index types?
Сигнатура индекса в объявлении интерфейса позволяет указать любое количество свойств для разных объектов, которые будут созданы в соответствии с данным интерфейсом. Сигнатуры индексов позволяют обращаться к свойствам с произвольным именем
Индексная сигнатура (index signature) состоит из двух частей. В первой части расположен имеющий собственную аннотацию типа идентификатор привязки (binding identifier) заключенный в квадратные скобки []. Во второй части расположена аннотация типа (type annotation) представляющего значение ассоциируемое с динамическим ключом.
В этом примере я использовал сигнатуру индекса для добавления информации о разных поселениях вокруг озер. Поскольку у каждого озера будут свои поселения, использование дополнительных объектов не было бы хорошей идеей.
interface Lakes {
name: string,
area: number,
length: number,
depth: number,
isFreshwater: boolean,
countries: string[],
frozen?: string[],
[extraProp: string]: any
}
let fourthLake: Lakes = {
name: 'Tanganyika',
depth: 1470,
length: 676,
area: 32600,
isFreshwater: true,
countries: ['Burundi', 'Tanzania', 'Zambia', 'Congo'],
kigoma:'Tanzania', // индексированное поле
kalemie: 'Congo',// индексированное поле
bujumbura: 'Burundi'// индексированное поле
}
Для чего нужен оператор keyof?
В TypeScript существует возможность выводить все публичные, не статические, принадлежащие типу ключи и на их основе создавать литеральный объединенный тип ( U nion ). Для получения ключей нужно указать оператор keyof , после которого указывается тип, чьи ключи будут объединены в тип объединение keyof Type .
type AliasType = { f1: number, f2: string };
interface IInterfaceType {
f1: number;
f2: string;
}
class ClassType {
f1: number;
f2: string;
}
let v1: keyof AliasType; // v1: "f1" | "f2" - тип из ключей типа
let v2: keyof IInterfaceType; // v2: "f1" | "f2" - тип из ключей интерфейса
let v3: keyof ClassType; // v3: "f1" | "f2" - тип из ключей класса
let v4: keyof number; // v4: "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString" - тип из ключей объекта
Еще одно объяснение
Оператор keyof иногда называют оператором запроса индекса, поскольку он запрашивает тип, указанный после него. Когда TypeScript видит оператор keyof в аннотации типа, он запрашивает тип после него и извлекает все его не публичные, не статические ключи. Затем он строит тип литерала строки объединения из этих ключей.
Для чего предназначены Mapped types? Как создать собственный Mapped type?
Для чего предназначен.
Сопоставленный тип — это процесс создания нового типа путем сопоставления информации о типе из существующего типа.
type MappedTypeName = { [K in UnionType]: ExistingType };Оператор in сопоставляет каждый элемент в типе объединения, чтобы создать новый тип. Другими словами, оператор in позволяет нам перебирать каждый тип в типе объединения. В итерации цикла каждый элемент в типе объединения помещается в K, который становится ключом в новом типе. Таким образом, тип объединения обычно является объединением строковых литералов. Аннотация типа в отображаемом типе — это тип, присвоенный каждому ключу в типе.
type ContactDetails = { [K in "name" | "email"]: string };Создает следующий тип:
Copy { name: string; email: string; }Для формирования сопоставленного типа можно использовать оператор keyof:
Copy type MappedTypeName = { [K in keyof ExistingType1]: ExistingType2; };Это создает новый тип, содержащий ключи другого типа..
Еще один пример, для чего нужны Mapped Types. Есть интерфейс и функция:
interface Form {
values: T;
errors: any;
}
const contactForm: Form<{ name: string; email: string }> = {
values: {
name: "Bob",
email: "bob@someemail.com"
},
errors: {
emailAddress: "Invalid email address"
}
};
console.log(contactForm);
Во вложенном объекте errors поле должно быть названо не emailAddress, а email (если бы было поле для ошибки в
имени, оно должно было бы называться name; если бы в объекте values были еще поля, кроме этих двух, то для них
должны были бы быть и поля в объекте errors).
- Внесем следующие изменения:
- в интерфейме Form поменяем тип any на { [K in keyof T]?: string } для свойства errors. Что это даст: теперь во вложенном объекте errors могут присутсвовать только свойства с такими же именами, как в объекте values. При этом они будут опциональными
- теперь TS выдает ошибку на поле emailAddress -
"Type '{ emailAddress: string; }' is not assignable to type '{ name?: string | undefined; email?: string | undefined; }'. Object literal may only specify known properties, and 'emailAddress' does not exist in type '{ name?: string | undefined; email?: string | undefined; }'". Она вызвана тем, что среди ключей объекта values нет ключа с именем emailAddress. Т.е. TS подсказывает, что поле emailAddress надо переименовать в email
interface Form {
values: T;
errors: { [K in keyof T]?: string };
}
const contactForm: Form<{ name: string; email: string }> = {
values: {
name: "Bob",
email: "bob@someemail.com"
},
errors: {
email: "Invalid email address"
}
};
Для чего нужны generics
Для того, чтобы разобраться в дженериках, давайте вспомним, как работают обычные типы в тайпскрипт. Простые типы, создаваемые в TS, работают по принципу констант или монолитов. Т.е. если у меня есть несколько интерфейсов, например User и Message, то я их использую только для сообщений и для пользователей соответственно. Каждый из этих интерфейсов может быть наделен какими-то свойствами:
interface User {
name: string;
age: number
}
interface Message {
id: number;
text: string
}
В современных front-end приложениях мы все эти данные загружаем по api, т.е. если нам нужно загрузить пользователя, то нам нужны еще дополнительные свойства, например MessageState:
interface UserState {
loading: boolean;
error: Error | null;
data: User
}
interface MessageState {
loading: boolean;
error: Error | null;
data: Message
}
Множество конструкций такого типа только усложняет наш код, так как отличаются сейчас стейты только тем, что UserState использует тип User, а MessageState - тип Message. Нежелание тратить усилия на описание монолитных типов такого плана послужило созданию дженериков. Дженерик в TS - это параметризованный тип, позволяющий объявлять параметры типа, являющиеся временной заменой конкретных монолитных типов, определение которых будет выполнено в момент использования.
Дженерики в TS объявляются при помощи пары угловых скобок <>, в которые через запятую записываются типы:
interface State<T, U, V, K>{} // определили интерфейс State, который использует четыре дженерик типа
Количество типов не ограничено.
Также дженерики можно использовать не только в интерфейсах, но и в типах, классах, функциях:
type State2<T, U, V, K> = {} // в типе
class State3 <T> {} // в классе
function state4 <T> (){} // в функции
Ведут себя дженерики везде одинаково, т.е. правила их сипользования идентичны, поэтому изучить их придется один раз, а использовать повсеместно.
Для того, чтобы разобраться, как работают дженерики, возьмем классический пример из официальной документации TS:
function identity(arg){
return arg;
}
const s: string = 'Hello';
const n: number = 10;
const r1 = identity(s); // r1 имеет тип any, хотя s имеет тип string
const r2 = identity(n); // r2 имеет тип any, хотя n имеет тип number
Т.к. мы передаем аргумент и аргумент того же типа ждем в ответ, и т.к. наша функция умеет работать с любыми типами, здесь как раз нам подойдут дженерики. Т.е. мы можем сказать, что функция identity работает с типом T, аргумент, который прилетает в функцию, будут типа T, при этом принадлежность этого типа Т устанавливается в момент передачи аргумента. Т.е. если передаем сюда S (строку), то Т будет тоже строкой и вернется значение тоже типа Т (строка):
function identity <T> (arg: T): Т {
return arg;
}
const s: string = 'Hello';
const n: number = 10;
const r1 = identity(s); // r1 имеет тип string
const r2 = identity(n); // r2 имеет тип number
При использовании дженериков тайпскрипт распознает типы переменных на лету, тем самым позволяя писать более универсальный код
const user: User = {
name: 'Max',
age: 20
}
const r3 = identity(user); // r3 имеет тип User
Давайте вернемся к нашим типам User и Message и создадим для них дженерик тип State:
interface State<T> { // указываем, что это интерфейс-дженерик
loading: boolean;
error: Error | null;
data: T
}
type UserState = State<User>
type MessageState = State<Message>
const messageState: MessageState = {
loading: false,
error: null,
data: {
id: 1,
text: 'Text'
}
}
Если дженерик указывается в качестве типа данных, то он обязан содержать аннотацию обобщения, т.е. <User> или <Message>. State сам по себе существовать не может, т.к. он ожидает на вход дженерик. Т.е. рассмотреть дженерик можно как функцию, которую мывызываем с определенным типом и она возвращает новую структуру с использованием пееданного типа.
А сейчас типизируем что-то чуть более сложное. Создадим функцию, которая будет возвращать случайный элемент массива генерируем случайный индекс и берем элемент по этому индексу:
function getRandomElement (items){
const randomIndex = Math.floor(Math.random() * items.length);
return items[randomIndex];
}
const el1 = getRandomElement([1,2,3,4,5]); // el1 имеет тип any
Используем дженерики. Переменная items это массив типов Т, возвращает функция тоже тип Т:
function getRandomElement <T> (items: T[]): T {
const randomIndex = Math.floor(Math.random() * items.length);
return items[randomIndex];
}
const el1 = getRandomElement([1,2,3,4,5]); // el1 имеет тип number
const el2 = getRandomElement(['a', 'b', 'c', 'd', 'e',]); // el1 имеет тип string
const el2 = getRandomElement(['a', 'b', 'c', 'd', 1,]); // el1 имеет тип string|number
Еще один пример с объектами
function merge (o1, 02){
return {...o1, ...o2};
}
const r1 = merge({a: 1}, {b: 2}); // r1 имеет тип any, т.к. никакие типы не указаны
Если мы хотим, чтобы TS распознавал типы налету, мы используем дженерик
function merge <U, V> (o1 <U>, 02 <V>) U & V{ //т.к. TS знает, что он возвращает объединение U и V ...
return {...o1, ...o2};
}
const r1 = merge({a: 1}, {b: 2}); //... он указывает, что тип r1 - это оъединение {a: number} & {b: number}
В TS существует множество встроенных дженериков. Самый популярный из них это дженерик Promise .
Функция fakeRequest всегда вернет промис, т.к. асинхронные функции всегда возвращают промис:
async function fakeRequest(){
return 2
}
Вызов fakeRequest без await. Будет ошибка "Type '() => Promise<number>' is not assignable to type 'number'" . Это значит, что нельзя записать число в промис, т.к. fakeRequest всегда вернет промис
const b:number = fakeRequest()
Поэтому надо использовать дженерик Promise
const b: Promise<number> = fakeRequest(); //Здесь, т.к. я использую дженерик как тип, я должен явно указать, какой тип будет использоваться
Другой пример
type Names = string[]; // массив строк
type Names = Array<string> // то же самое, но используя дженерик
Также существует дженерик для создания объекта . Например мы хотим создать объект, у которого ключи будут строками, а значения - числами
type Obj = {
[key: string]: number
}
/*чтобы не делать запись, как запись выше, можно использовать встроенный дженерик*/
type Obj = Record<string, number> // ключи - строки, значения - числа
Помимо того, что в дженериках параметры можно указывать в качестве конкретного типа, они также могут расширять другие типы, в т.ч. и другие параметры типа. Такой механизм требуется для ограничения наборов обязательных признаков для выполнения операций
Пример. Функция len, которая принимает массивоподобный объект и возвращает его длину:
function len(collection){
return collection.length;
}
можно получить длину у строки, у массива, но у числа получить ее не могу:
const r1 = len('Hello'); // ok
const r1 = len([1, 2, 3]); // ok
const r1 = len(1); // error
С помощью дженерика я могу указать, что моя коллекция будет ограничена (extends) только теми структурами данных, у которых есть свойство length и оно имеет тип number
function len <T extends {length: number}> (collection){
return collection.length;
}
Более юзабельный кейс - у нас есть класс DataCollection и он умеет работать с разными коллекциями. У него есть конструктор, который создает публичное свойство data, для начала опишем его с any; И у него есть метод search, который выполняет поиск по ID в массиве data
class DataCollection {
constructor(public data: any[]){}
search (id: string) {
return this.data.find(d => d.id === id);
}
}
const users = new DataCollection ([{
id: '1'
}]); // users имеет тип DataCollection
const messages = new DataCollection ([{
text: 'Message' // даже если не будет свойства id, то TS не поругается, т.к. он не знает, что мы работаем с ID, хотя код работать не будет
}])
messages.search(1); // вернется null
Поэтому для того, чтобы уметь работать с разными структурами данных, мы можем использовать дженерик. Т.к. users и messages имеют разные типы, но из общего можно определить id. Поэтому можно сказать, что DataCollection работает с любой структурой данных, у которой есть свойтсво id и оно string
class DataCollection <T extends {id: string}>{
constructor(public data: T[]){}
search (id: string) {
return this.data.find(d => d.id === id); // ошибка - 'Property id does not exist on type 'T''
}
}
type User = {
id: string,
name: string
}
type Message = {
id: string,
text: string
}
const users = new DataCollection ([{
id: '1'
}]); // users имеет тип DataCollection
const messages = new DataCollection ([{
text: 'Message' // теперь здесь будет ошибка, что это невалидный объект, т.е. нужно добавить свойство id
}])
/*добавляем id, также мы можем определить у этого объекта тип */
const message: Message = {
text: 'Message',
id: '2'
}
const messages = new DataCollection ([message]); // ok
const searchableMessage = messages.search(1); // searchableMessage имеет тип Message|undefined
/*точно такая же ситуация с users*/
const user: User = {
text: 'Message',
id: '2'
}
const users = new DataCollection ([users]); // ok
const searchableUsers = users.search(1); // searchableUsers имеет тип User|undefined
Еще один пример - нам нужно просто получать свойство из объекта. Создадим function, назовем ее getValue.
function getValue(obj, prop){
return obj[prop];
}
const r1 = getValue({
name: 'Max'
}, age); // свойства age нет, но TS просто указывает на то, что вернется any и сам разбирайся, что с ним делать
Но используя дженерик мы можем определить тип объекта, который нам приходит, и указать, что он должен быть только объект, т.е. ничего другого туда передавать нельзя
function getValue <T extends object>(obj: Т, prop){
return obj[prop];
}
если передать первым параметром строку, то TS будет ругаться, говорить, что нельзя передать строку, т.к. нам нужен object т.е. мы c помощью extends ограничиваем набор типов, которые могут быть переданы в эту функцию
const r1 = getValue('$', age);
для второго параметра укажем, что второй аргумент будет иметь тип U, а U будет ограничен ключами Т. extends в этом случае знает, что в переменную Т может быть записан только то, что может быть записано в объект, а в U может быть записано только то, что является ключом Т, т.е. нашего объекта
function getValue <T extends object, U extends keyof T>(obj: Т, prop: U){
return obj[prop];
}
const r1 = getValue({
name: 'Max'
}, name); // вторым аргументом можем указать только name, т.к. в объекте только один ключ name.
Если же в объекте будет больше ключей, то можем указать любой из них
const r1 = getValue({
name: 'Max',
age: 20
}, age); // ok
Теперь создадим функцию, которая возвращает ключ, а не свойство. Т.е. все то же самое, тоже передаем объект, но теперь мы спрашиваем value и возвращаем ключ
/*obj - объект, value - не ключ объекта, как предыдущем случае, а его value;
возвращать будем ключ или null*/
function getKey<T extends object, U extends keyof T> (obj: T, value: T[U]): U|null {
/*Object.keys прокидывает строку, но нам нужно сказать, что массив U*/
const key = obj.keys(obj) as Array<U>.find(k => obj[k]===value) // перебираем ключи и находим то значение, кот. совпадает с переданным
}
return key || null ;
const key = getKey ({ // key имеет тип "name"|null
name: 'Max'
}, 'Max')
/*другой вариант записи*/
function getKey<T extends object> (obj: T, value: T[keyof T]): keyof T|null {
/*Object.keys прокидывает строку, но нам нужно сказать, что массив U*/
const key = obj.keys(obj) as Array<keyof T>.find(k => obj[k]===value) // перебираем ключи и находим то значение, кот. совпадает с переданным
}
return key || null ;
Еще один пример
Нужно создать функцию, которая будет называться patchField, она будет принимать:
1) объект,
2) свойство объекта, которое нужно пропатчить (поменять).
3) значение, на которое нужно поменять
При этом patchField должна быть функцией безопасной, т.е. если тип данных значения объекта, из которго берется пропатчиваемое свойство - это number, то его можно поменять только на number, если string - то толькл на string и т.д.
/*У нас будет три дженерика*/
patchField<
T extends object, // принимаем объекты первым аргументом
U extends keyof T, // принимаем ключи вторым аргументом
V extends T[U]// принимаем тип значения, которое мы хоти поменять, третьим аргументом. Т.е. нашли объект, нашли его ключ и взяли его значение
>(obj: T, field: U, value: V)
Также TS позволяет указывать для параметров типа значения по умолчанию
function format<T>(s?: T): T|undefined{
return s;
}
const s1 = format(); // s1 имеет тип unknown
/*задаем дефолтное значение*/
function format<T=string>(s?: T): T|undefined{
return s;
}
Пример для закрепления, который использует все возможности дженериков. Создадим тип FunctionalComponent, который будет аналогом реакт FC
тип FunctionalComponent, который будет принимать дженерик, и в реакте каждый компонент у нас имеет свойство children. Поэтому мы укажем, что это будет object, по дефолту он тоже будет оъектом, если он не передан. Это будет функция (т.к. все компоненты в реакте это функции), которая принимает пропсы типа Т и добавляет к ним children, который будет any и возвращает any
type FunctionalComponent<T extends object = object> = (props: T & children: any) => any;
/*Теперь мы можем создать компонент типа FunctionalComponent, он будет принимать name типа string и age типа number*/
const component: FunctionalComponent<{name: string, age: number}> = ({
/*здесь я могу достать age, name, children, т.к. этот дженерик длбавил к объекту типа {name: string, age: number } - children: any */
age,
name,
children
}) => {
}
Обобщение (Generics) — это параметризированный тип позволяющий объявлять параметры типа, являющиеся временной заменой конкретных типов, конкретизация которых будет выполнена в момент создания экземпляра.
Принадлежность параметра типа к конкретному типу данных устанавливается в момент передачи аргументов типа. При этом конкретные типы данных указываются в паре угловых скобок, а количество конкретных типов должно соответствовать количеству обязательных параметров типа.
Какие есть встроенные обобщенные интерфейсы (built-in generic types)?
Readonly
const в JavaScript означает, что вы не можете переназначить этому имени какие-либо другие значения. Однако const позволяет изменять свойства объекта. Тут на помощь приходит тип Readonly:
type Point = {
x: number,
y: number
};
const p: Readonly<Point> = {
x: 4,
y: 2
};
p.x = 5; // ⚡️ compile error!
ReadonlyArray
Встроенный generic тип ReadonlyArray позволяет выдавать ошибки при использовании функции массива, которая изменяет исходный массив:
const values: ReadonlyArray<number> = [1, 2, 3, 4, 5];
values.push(6); // ⚡️ compile error! This mutates the array
values.filter(x => x > 4); // ✔︎ compiles! filter returns a new array
Полезно, если вы хотите иметь неизменяемый массив
ReturnType
Передает возвращаемый тип любой функции:
type Point = {
x: number,
y: number
}
function generateSquare(bottomLeft: Point, topRight: Point) {
return {
bottomLeft,
bottomRight: {
x: topRight.x,
y: bottomLeft.y,
},
topLeft: {
x: bottomLeft.x,
y: topRight.y
},
topRight
}
}
type Square = ReturnType<typeof generateSquare>
// Теперь можно использовать этот возвращенный тип везде
function areaOfSquare(square: Square): number {
//do something
return result;
}
Вы также можете получать доступ к функциям внутри класса:
class Square {
generate(bottomLeft, topRight) {
return {
bottomLeft,
bottomRight: {
x: topRight.x,
y: bottomLeft.y,
},
topLeft: {
x: bottomLeft.x,
y: topRight.y
},
topRight
}
}
}
type TSquare = ReturnType<Square['generate']>
declare let result: TSquare;
Partial
Partial<T> берет все свойства одного типа и делает их необязательными. Для чего это? Подумайте о том, что у вас есть набор параметров по умолчанию, и вы хотите переопределить только его части. Partial<T> поможет вам получить автозаполнение и проверку типов для этого случая:
const defaultOptions = {
directory: '.',
incremental: true,
filePattern: '**/*',
}
function start(options: Partial<typeof defaultOptions>) {
const allOptions = Object.assign({}, defaultOptions, options);
console.log(allOptions);
}
start({
incremental: false, // Awesome! Typechecks!
});
start({
flatten: true // ⚡️ Error! This property has nothing to do with our options
});
Required
Это противоположность Partial<T>. Partial<T> делает каждое свойство опциональным, а Required<T> делает каждое свойство обязательным.
type Container = {
width: number,
height: number,
children?: Container[]
}
function getChildrenArea(parent: Required<Container>) {
let sum = 0;
for (let child of parent.children) {
sum = sum + (child.width * child.height)
}
return sum;
}
const mainContainer: Container = {
width: 200,
height: 100
}
getChildrenArea(mainContainer); // ⚡️ Error: Needs children
NonNullable
NonNullable<T> помогает быть уверенным, что вы не передаете null или undefined функциям. Это дополняет флаг компилятора strictNullChecks, поэтому обязательно активируйте его.
function print<T>(x: NonNullable<T>) {
console.log(x.toString());
}
print('Hello');
print(2);
print(null); // ⚡️ Error
print(undefined); // ⚡️ Error
Pick
С помощью Pick<T, K extends keyof T>, вы можете создать новый тип из существующего объекта, используя только выбранный список свойств. Одноименная функция pick Lodash — хороший пример ее использования:
/**
* The pick function is generic as well. It has two generic types:
* - T ... the type of the object we want to pick props from
* - K ... a subset of all keys in T
*
* Our method signature takes an object of type T, the other parameters
* are collected in an array of type K.
*
* The return type is a subset of keys of T.
*/
declare function pick<T, K extends keyof T>(obj: T, ...propsToPick: K[]): Pick<T, K>
const point3D = {
x: 2,
y: 0,
z: 4
}
const point2D = pick(point3D, 'x', 'y'); // returns a type { x: number, y: number }
Она особенно полезна, когда используется с другими дженериками, наример Exclude
Record
Record<K, T> - с ее помощью вы можете сказать, что каждый ключ в К должен быть типа Т:type Person = Record<'firstName' | 'lastName', string>
что то же самое что и
{ firstName: string, lastName: string } Или
type MetaInfo = {
title: string,
url: string
}
type Episodes = Record<string, MetaInfo> - разрешает объект с ключами любого типа, но
со значениями типа MetaInfo. Это очень похоже на
{ [k: string]: MetaInfo }
Пока все хорошо. Но зачем нам эти generic типы Record, если мы можем добиться того же результата другими методами?
Ответ: Record нужен, когда вы имеете дело с другими generic типами.
Посмотрим на пример: мы можем создать функцию, которая преобразует все значения объекта в строковое представление.
// Реализация находится в другом месте. Она преобразует все значения в строки.
declare function allToString<T>(obj: T): Record<keyof T, string>
const person = {
firstName: 'Stefan',
lastName: 'Baumgartner',
age: Number.MAX_VALUE
}
// Все свойства в strPerson теперь строки
const strPerson = allToString(person);
Extract
Extract<T, K> извлекает все типы из Т, если их можно присоить К. Скажем, у вас есть два различных типа формы - круги и четырехугольники:
const rect = {
width: 200,
height: 100,
area() {
return this.width * this.height;
},
perimeter() {
return this.width * 2 + this.height * 2;
}
}
const circle = {
r: 50,
area() {
return this.r * this.r * Math.PI;
},
perimeter() {
return 2 * this.r * Math.PI;
}
}
Их типами являются:
type Rect = typeof rect;
type Circle = typeof circle;
Они имеют нечто общее: оба имеют методы для вычисления площади и периметра (area и perimeter). Эти объекты могут
измениться со временем, но мы хотим быть уверенными, что всегда имеем доступ только к методам, которые существуют
в них обоих. При помощи Extract мв можем получить ключи этих функций
// extracts: 'area' | 'perimeter'
type ShapeFunctionProps = Extract<keyof Rect, keyof Circle>
Чтобы создать тип, у которого есть доступ ко всем функциям, еще раз, мы можем использовать тип Record
type ShapeFunctions = Record<ShapeFunctionProps, () => number>
Теперь у нас есть дополнительная типобезопасность в функциях, которая может быть использована во всех этих
объектах
declare function extensiveMath<T extends ShapeFunctions>(obj: T)
extensiveMath(rect); // ✅ compiles
extensiveMath(circle); // ✅ compiles
Exclude
Exclude<T, K> исключает все типы из Т, которые могут быть назначены К. Т.е. оно противоположно Extract. Вернемся к примеру, рассмотренному ранее:
type Rect = typeof rect;
type Circle = typeof circle;
// есть только 'width' | 'height';
type RectOnlyProperties = Exclude<keyof Rect, keyof Circle>
// объект типа { width: number, height: number }
type RectOnly = Pick<Rect, RectOnlyProperties>
declare function area<T extends RectOnly>(obj: T)
area({
width: 100,
height: 100
}) // ✅ compiles;
Omit
Omit<T, K> это шорткат Pick<T, Exclude<keyof T, K>>:
type Person = {
name: string;
age: number;
location: string;
};
type Cosmopolitan = Omit<Person, 'location'>
// equivalent to
type Cosmopolitan = {
name: string;
age: number;
};
Parameters<T> - получить тип размеченного кортежа, описывающий параметры функционального типа
InstanceType
Как ограничить возможные значения переменной типа?
Иногда возникает необходимость в создании дженерика, работающего с набором типов, когда мы имеем некоторую информацию о возможностях, которыми будет обладать этот набор. В нашем примере loggingIdentity мы хотим получать доступ к свойству length аргумента arg , но компилятор знает, что не каждый тип имеет такое свойство, поэтому не позволяет нам делать так:
function loggingIdentity(arg: Type): Type {
console.log(arg.length)
// Property 'length' does not exist on type 'Type'.
return arg
}
Мы хотим, чтобы функция работала с любым типом, у которого имеется свойство length. Для этого мы должны
создать ограничение типа. Нам необходимо создать интерфейс, описывающий ограничение. В следующем примере мы
создаем интерфейс с единственным свойством length и используем его с помощью ключевого слова extends для
применения органичения:
interface Lengthwise {
length: number
}
function loggingIdentity(arg: Type): Type {
console.log(arg.length)
// Теперь мы можем быть увереными в существовании свойства `length`
return arg
}
Поскольку дженерик был ограничен, он больше не может работать с любым типом:
loggingIdentity(3) // Ошибка - аргумент типа 'number' не может быть присвоен параметру типа 'Lengthwise'
Мы должны передавать ему значения, отвечающие всем установленным требованиям:
loggingIdentity({ length: 10, value: 3 })
Источник
Как ограничить возможные значения переменной типа значениями другой переменной типа?
Мы можем определять типы параметров, ограниченные другими типами параметров. В следующем примере мы хотим получать свойства объекта по их названиям. При этом, мы хотим быть уверенными в том, что не извлекаем несуществующих свойств. Поэтому мы помещаем ограничение между двумя типами:
function getProperty(obj: Type, key: Key) {
return obj[key]
}
const x = { a: 1, b: 2, c: 3, d: 4 }
getProperty(x, 'a')
getProperty(x, 'm')
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Еще один пример: напишем функцию, которая принимает в себя объект и ключ, а выдает значение из переданного объекта
по ключу
function getPropertyValue(obj: Obj, key: Key): Obj[Key] { //тип Key ограничен типом keyof Obj
return obj[key];
}
В этом примере также показана возможность ограничения типа, используемого в объявлении функции, с помощью уже
имеющегося параметра
Можно ли для переменной типа указать значение по умолчанию?
Можно
function format<T=string>(s?: T): T|undefined{
return s;
}
Параметры, которым заданы значения по умолчанию, являются необязательными параметрами. Необязательные параметры типа должны быть перечислены строго после обязательных. Если параметр типа указывается в качестве типа по умолчанию, то ему самому должно быть задано значение по умолчанию, либо он должен расширять другой тип.
Исправьте типизацию функции filterBy таким образом, чтобы она:
a. первым аргументом принимала массив любых объектов;
b. вторым – имя поля, по которому производится фильтрация;
c. третьим – значение, которое должно содержать поле;
d. возвращала тип первого аргумента (исходного массива).
// исходная функция
function filterBy(input: Object[], propName: string, propValue: any): Object[] {
return input.filter(item => item[propName] === propValue);
}
// что должно получиться
interface IEmployee {
name: string;
age: number;
position: 'Programmer' | 'Accountant' | 'Designer';
}
const employees: IEmployee[] = [
{ name: 'Michael', age: 20, position: 'Programmer' },
{ name: 'Jordan', age: 25, position: 'Designer' },
{ name: 'Steve', age: 34, position: 'Accountant' },
{ name: 'Tom', age: 19, position: 'Programmer' },
{ name: 'Bob', age: 43, position: 'Programmer' },
{ name: 'Michael', age: 19, position: 'Programmer' },
{ name: 'Bob', age: 27, position: 'Designer' },
];
filterBy(employees, 'position', 'Programmer'); // вернёт IEmployee[]
filterBy(employees, 'surname', 'Cook'); // ошибка, тип IEmployee не содержит поле 'surname'
filterBy(employees, 'position', 'Tester'); // ошибка, поле 'position' не может содержать значение 'Tester',
Решение:
function filterBy
(input: O[], propName: U, propValue: O[U]): O[] {
return input.filter((item) => item[propName] === propValue);
}
Не понятно, почему не работают вот эти варианты
//1
function filterBy
(input: T, propName: U, propValue: O[U]): O[] {
return input.filter((item) => item[propName] === propValue);
}
//2
function filterBy
(input: O[], propName: U, propValue: O[U]): O[] {
return input.filter((item) => item[propName] === propValue);
}
Типизируйте класс List так, чтобы:
Конструктор класса принимал массив объектов с обязательным полем id;
Метод addItem позволял добавить только объект с типом аргумента конструктора;
Метод getList возвращал массив объектов с типом аргумента конструктора;
// Исходный класс
class List {
private list;
constructor(list) {
this.list = list;
}
addItem(item) {
this.list.push(item);
}
getList() {
return this.list;
}
}
// Что должно получиться
interface IGuest {
login: string;
password: string;
}
const guests: IGuest[] = [
{
login: 'guest',
password: '123',
},
{
login: 'user',
password: '123',
}
];
const guestsList = new List<IGuest>(guests); // ошибка, в типе IGuest отсутствует поле id
interface IUser {
id: number;
login: string;
password: string;
}
const users: IUser[] = [
{
id: 1,
login: 'guest',
password: '123',
},
{
id: 2,
login: 'user',
password: '123',
},
{
id: 3,
login: 'author',
password: '123',
}
];
const usersList = new List<IUser>(users); // ok
usersList.addItem({
login: 'guest',
password: '123',
}); // ошибка, отсутствует поле id
usersList.addItem({
id: 3,
login: 'admin',
}); // ошибка, отсутствует поле password
usersList.addItem({
id: 5,
login: 'guest',
password: '123',
}); // ok
const usersArray = usersList.getList(); // IUser[];
Решение:
class List<T extends {id: unknown}> {
private list;
constructor(list: T[]) {
this.list = list;
}
addItem(item: T) {
this.list.push(item);
}
getList() {
return this.list;
}
}
По каким правилам выводятся типы дженерик аргументов при вызове функций в следующих случаях: - ???????
если при вызове функции явно передан дженерик аргумент
если дженерик аргумент не передан и этот аргумент используется для типизации аргумента функции
если дженерик аргумент не передан и этот аргумент используется для типизации возвращаемого функцией значения
Обычно пишут функцию, в которой типы входных данных связаны с типом выходных данных или где типы двух входных данных каким-то образом связаны. Давайте на мгновение рассмотрим функцию, которая возвращает первый элемент массива:
function firstElement(arr: any[]) {
return arr[0];
}
Эта функция выполняет свою работу, но, к сожалению, имеет возвращаемый тип any. Лучше бы функция возвращала тип
элемента массива. В TypeScript дженерики используются, когда мы хотим описать соответствие между двумя значениями.
Мы делаем это, объявляя параметр типа в сигнатуре функции:
function firstElement(arr: Type[]): Type | undefined {
return arr[0];
}
Добавив к этой функции параметр типа Type и используя его в двух местах, мы создали связь между входом функции
(массивом) и выходом (возвращаемым значением). Теперь, когда мы вызываем его, получается более конкретный тип:
// s имеет тип 'string'
const s = firstElement(["a", "b", "c"]);
// n имеет тип 'number'
const n = firstElement([1, 2, 3]);
// u имеет тип undefined
const u = firstElement([]);
Обратите внимание, что в этом примере нам не нужно было указывать Type. Тип был выведен — выбран автоматически — с
помощью TypeScript. Мы также можем использовать несколько параметров типа. Например, автономная версия карты будет
выглядеть так:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// Parameter 'n' имеет тип 'string'
// 'parsed' имеет тип 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
Обратите внимание, что в этом примере TypeScript может вывести как тип параметра типа ввода (из заданного массива
строк), так и параметр типа вывода на основе возвращаемого значения выражения функции (числа).
Как можно использовать обобщения в реакт компонентах?
СтатьяUnion и intersection типы
Для чего нужны?
Позволяют скомбинировать существующие типы вместо того, чтобы создать их с нуля
Как использовать?
Объединение (Union) — это механизм, позволяющий создавать из множества существующих типов логическое условие, по которому данные могут принадлежать только к одному из указанных типов. Объединение указывается с помощью оператора прямой черты |, по обе стороны которой располагаются типы данных.
let v1: T1 | T2 | T3;
Переменной, которой был указан тип объединения A или B или C, может быть присвоено значение, принадлежащие к одному из трех типов.
Пересечение (Intersection) — ещё один механизм TypeScript, который позволяет рассматривать множество типов данных как единое целое. Пересечение указывается с помощью оператора амперсанда & по обе стороны от которого указываются типы данных.
let v1: T1 & T2 & T3;
Переменной, которой был указан тип пересечение A и B и С должно быть присвоено значение принадлежащее к типам A и B и C одновременно. Другими словами значение должно обладать всеми обязательными признаками каждому типа определяющего пересечение.
Пояснение человеческим языком Читать здесь >> и здесьЧто такое Type Guards? Для чего они нужны?
Защитники типа — это выражения сужения диапазона типов Union до одного из значений, а процесс приведения определенного типа к более конкретной версии с помощью защитников типа и присвоений называется сужением типа (narrowing)
Подробнее Здесь
Предположим, что у нас имеется функция под названием padLeft:
function padLeft(padding: number | string, input: string): string {
throw new Error('Еще не реализовано!')
}
Если padding — это number, значит, мы хотим добавить указанное количество пробелов перед input. Если padding —
это string, значит, мы просто хотим добавить padding перед input. Попробуем реализовать логику, когда padLeft
принимает number для padding:
function padLeft(padding: number | string, input: string): string {
return new Array(padding + 1).join(' ') + input
// Operator '+' cannot be applied to types 'string | number' and 'number'. Оператор '+' не может быть применен к типам 'string | number'
}
Мы получаем ошибку. TS предупреждает нас о том, что добавление number к number | string может привести к
неожиданному результату, и он прав. Другими словами, мы должны проверить тип padding перед выполнением
каких-либо операций с ним
Как работают основные Type Guards:
оператор in
В выражении 'value' in x, где 'value' — строка, а x — объединение, истинная ветка сужает типы x к типам, которые имеют опциональное или обязательное свойство value, а ложная ветка сужает типы к типам, которые имеют опциональное или не имеют названного свойства:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
typeof
- Оператор typeof возвращает одну из следующих строк:
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
if (typeof strs === "object") {
//
}
instanceof
Оператор instanceof используется для определения того, является ли одна сущность "экземпляром" другой. Например, выражение x instanceof Foo проверяет, содержится ли Foo.prototype в цепочке прототипов x. Данный оператор применяется к значениям, сконструированным с помощью ключевого слова new. Он также может использоваться для сужения типов:
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
// (parameter) x: Date
} else {
console.log(x.toUpperCase());
// (parameter) x: string
}
}
сравнение ==/===
function example(x: string | number, y: string | boolean) { if (x === y) { /*Теперь мы можем вызывать любой строковый метод, т.к. знаем, что x и y - строки. Когда мы сравниваем значения x и y, TS знает, что их типы также должны быть равны. Поскольку string — это единственный общий тип, которым обладают и x, и y, TS знает, что x и y должны быть string в первой ветке.*/ }отрицание ! и двойное отрицание !!
Явно преобразовать значение в логическое можно с помощью функции Boolean или с помощью двойного отрицания (!!):
// оба варианта возвращают `true`
Boolean('hello')
!!'world'
Оператор ! - логическое отрицание (НЕ):
if (!values) {
//
}
Как объявить свой Type Guard?
Для определения пользовательского защитника типа необходимо определить функцию, возвращаемым значением которой является предикат типа:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
pet is Fish — это наш предикат. Предикат имеет форму parameterName is Type , где
parameterName — это название параметра из сигнатуры текущей функции. При вызове isFish с любой
переменной, TS "сузит" эту переменную до указанного типа, разумеется, при условии, что оригинальный тип
совместим с указанным.
const pet = getSmallPet()
if (isFish(pet)) {
pet.swim()
} else {
pet.fly()
}
Что такое Assertion Functions и для чего они нужны?
Механизм утверждения в сигнатуре позволяет определять утверждающие функции, вызов которых, в случае невыполнения условия, приводит к выбрасыванию исключения
Ключевой особенностью утверждения в сигнатуре является то, что в качестве аргумента утверждающая функция ожидает выражение, определяющие принадлежность к конкретному типу с помощью любого предназначенного для этого механизма (typeof, instanceof и даже с помощью механизма утверждения типов, реализуемого самим TypeScript). Если принадлежность значения к указанному типу подтверждается, то далее по коду компилятор будет рассматривать его в роли этого типа. Иначе выбрасывается исключение.
// утверждение в сигнатуре
function isStringAssert(condition: any): asserts condition {
if (!condition) {
throw new Error(``);
}
}
// утверждение типа
function isString(value: any): value is string {
return typeof value === 'string';
}
const testScope = (text: any) => {
text.toUppercase(); // до утверждения расценивается как тип any..
isStringAssert(text instanceof String); // выражение с оператором instanceof
isStringAssert(typeof text === 'string'); // выражение с оператором typeof
isStringAssert(isString(text)); // механизм "утверждения типа"
text.toUppercase(); // ..после утверждения, как тип string
}
Что такое Discriminated union?
Тип Discriminated Unions (дискриминантное объединение), часто обозначаемое как Tagged Union (размеченное объединение), так же как и тип union (объединение), является множеством типов, перечисленных через прямую черту |. НО значение, ограниченное дискриминантным объединением, может принадлежать только к одному типу из множества.
- Discriminated Union похож на тип Union, но существует два отличия:
- типу Discriminated Union могут принадлежать только ссылочные типы данных
- каждому объектному типу (также называемые варианты), составляющему Discriminated Union, указывается идентификатор варианта - дискриминант. По этому дискриминанту вывод типов понимает, к какому именно типу из перечисленных в объединении относится данный конкретный тип
Отличия простого объединения и дискриминантного на примере.
Простое объединение
class Bird {
fly(): void {}
toString(): string {
return 'bird';
}
}
class Fish {
swim(): void {}
toString(): string {
return 'fish';
}
}
class Insect {
crawl(): void {}
toString(): string {
return 'insect';
}
}
function move(animal: Bird | Fish | Insect): void {
animal.fly(); // Error -> [*]
animal.swim(); // Error -> [*]
animal.crawl(); // Error -> [*]
animal.toString(); // Ok -> [*]
/**
* [*]
*
* Поскольку вывод типов не может
* определить к какому конкретно
* из трех типов принадлежит параметр
* animal, он не позволяет обращаться к
* уникальным для каждого типа членам,
* коими являются методы fly, swim, crawl.
*
* В отличии от этих методов, метод toString
* определен в каждом из возможных типов,
* поэтому при его вызове ошибки не возникает.
*/
}
дискриминантное объединение
class Bird {
type: 'bird' = 'bird'; // дискриминант
fly(): void {}
toString(): string {
return 'bird';
}
}
class Fish {
type: 'fish' = 'fish'; // дискриминант
swim(): void {}
toString(): string {
return 'fish';
}
}
class Insect {
type: 'insect' = 'insect'; // дискриминант
crawl(): void {}
toString(): string {
return 'insect';
}
}
function move(animal: Bird | Fish | Insect): void {
//Здесь TS понимает, что объект должен быть данного конкретного типа и делает сужение типа
if (animal.type === 'bird') {
animal.fly(); // Ok
// и здесь
} else if (animal.type === 'fish') {
animal.swim(); // Ok
// и здесь
} else {
animal.crawl(); // Ok
}
animal.toString(); // Ok
}
Каким может быть тип дискриминанта?
Значение дискриминанта должно принадлежать к литеральному типу отличному от unique symbol и определенное в каждом типе, составляющем дискриминантное объединение. Это поле обязательно должно быть инициализировано при объявлении или в конструкторе.
- Литеральные типы, к которым может принадлежать дискриминант:
- Literal Number
- Literal String,
- Template Literal String
- Literal Boolean
- Literal Enum
Можно ли использовать проверку по дискриминанту в качестве Type Guard?
Для того, чтобы ответить на этот вопрос, необходимо вспомнить, что такое Type Guard (защитник типа)
Защитники типа — это правила, которые помогают выводу типов определить суженый диапазон типов для значения, принадлежащего к типу Union. Другими словами, разработчику предоставлен механизм, позволяющий с помощью выражений составить логические условия, проанализировав которые, вывод типов сможет сузить диапазон типов до указанного и выполнить над ним требуемые операции.
Таким образом, проверка по дискриминанту - это Type Guard
Что такое exhaustiveness checking? Зачем это нужно? Как этого добиться?
exhaustiveness checking - полная проверка. Она используется, чтобы убедиться, что все члены объединения имеют проверку и соответствующий ей обработчик, т.е. ничего не пропущено. Например: сначала были только интерфейсы Square и Rectangle и соответственно - тип type Shape = Square | Rectangle. И была функция с ветками только для square и rectangle
function area(s: Shape) {
if (s.kind === "square") {
return s.size * s.size;
}
else if (s.kind === "rectangle") {
return s.width * s.height;
}
}
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
Потом добавляется еще один интерфейс Circle
interface Circle {
kind: "circle";
radius: number;
}
Circle также добавляется в тип-объединение
type Shape = Square | Rectangle | Circle;
А в функции изменений нет (ветку для нового кейса не добавили)
function area(s: Shape) {
if (s.kind === "square") {
return s.size * s.size;
}
else if (s.kind === "rectangle") {
return s.width * s.height;
}
/*Нам бы хотелось, чтобы в этом месте была проверка на такие "неучтенные" кейсы, когда в объединении появляются новые члены
(т.е. чтобы TS выбрасывал ошибку и мы бы понимали, что тип в оъединение добавили, а условие, по которому он должен определяться и
обрабатываться - забыли). */
}
Поэтому добавим такой код: const _exhaustiveCheck: never = s;
function area(s: Shape) {
if (s.kind === "square") {
return s.size * s.size;
}
else if (s.kind === "rectangle") {
return s.width * s.height;
}
else {
/* ERROR : `Circle` is not assignable to `never` - код добавили и теперь TS выбрасывает ошибку и мы
понимаем, что забыли добавить условие для нового типа*/
const _exhaustiveCheck: never = s;
}
}
Добавим в функцию условие для "circle"
if (s.kind === "square") {
return s.size * s.size;
}
else if (s.kind === "rectangle") {
return s.width * s.height;
}
else if (s.kind === "circle") {
return Math.PI * (s.radius **2);
}
else {
// Теперь ошибки нет
const _exhaustiveCheck: never = s;
}
}
Больше о discriminated unions здесь
Для чего предназначены Conditional Types?
Условные типы (Conditional Types) - это типы, способные принимать одно из двух значений, основываясь на выражении,в котором устанавливается принадлежность к заданному типу данных. Условные типы семантически схожи с тернарным оператором.
T extends U ? T1 : T2
type T0<T> = T extends number ? string : boolean;
let v0: T0<5> // let v0: string
let v1: T0<'text'> // let v1: boolean
Как проявляется дистрибутивность в Conditional Types?
Условные типы, которым, в качестве аргумента типа, устанавливается объединенный тип (Union Type), называются распределительные условные типы (Distributive Conditional Types). Называются они так потому, что каждый тип, составляющий объединенный тип, будет распределен таким образом, чтобы выражение условного типа было выполнено для каждого.
type T0<T> =
T extends number ? "numeric" :
T extends string ? "text" :
"other";
let v0: T0< string | number > // let v0: "numeric" | "text"
let v1: T0< string | boolean > // let v1: "text" | "other"
// так видит разработчик
type T0<T> =
T extends number ? "numeric" :
T extends string ? "text" :
"other";
let v0: T0< string | number > // let v0: "numeric" | "text"
let v1: T0< string | boolean > // let v1: "text" | "other"
// так видит компилятор
type T0<T> =
// получаем первый тип составляющий union тип ( в данном случаи number ) и начинаем подставлять его на место T
number extends number ? "numeric" : // number соответствует number? Да! Определяем "numeric"
T extends string ? "text" :
"other"
// закончили определять один тип, приступаем к другому, в данном случаи string
string extends number ? "numeric" : // string соответствует number? Нет! Продолжаем.
string extends string ? "text" : // string соответствует string? Да! Определяем "text".
"other"
// Итого: условный тип T0<string | number> определен, как "numeric" | "text"