Конструкции языка

Приведение типов

До этого момента мы ни разу не говорили о том, как изменять тип значения. В TypeScript есть несколько способов изменения типа. Один из них - это приведение типа или каст (cast). Каст изменяет тип значения на тот, который был указан. Существует два синтаксиса для каста - с использованием <> и as.

const a: number = 10;

//Без cast-а
const b1: 10 = a; // Error: Type 'number' is not assignable to type '10'.

//cast через <>
const b2: 10 = <10>a;

//cast через as
const b3: 10 = a as 10;

Каст через <> и через as - это эквивалентные действия. Однако каст через <> не будет работать в .tsx файлах (это как .jsx файлы, только на TypeScript), потому что такой каст будет интерпретироваться как компонент React. Поэтому предпочтительней использовать as каст.

По своей сути каст - это ручное преобразование одного типа в другой. Это задача возникает например тогда, когда нужно более общий тип преобразовать к более конкретному: из number получить числовой литерал, от базового класса перейти к конкретному наследнику и так далее.

abstract class Base {
    public a: number = 10;
}

class A extends Base {
    public b: number = 15;
}

function some(b: Base) {
    const a: A = b as A;
}

some(new A());

Каст может создавать проблемы. TypeScript следит за тем, чтобы преобразование одного типа осуществлялось в совместимый тип. Например, нельзя преобразовать number в string.

Однако это ограничение можно обойти, сделав сначала каст до any, а потом до нужного типа.

Иногда преобразование в несовместимый тип приходится делать. Но кастом через any не стоит злоупотреблять. Использование подобной конструкции может вызвать проблемы в runtime. После приведения типов, TypeScript использует для компиляции приведенный тип. Однако значение, тип которого был изменен (особенно через as any as) может быть не совместимо с операциями, которые можно делать с приведенным типом. Например, не иметь каких то методов. В таком случаи ошибка возникнет в runtime, а не на этапе компиляции.

Для того, чтобы сломать runtime при помощи каста, необязательно преобразовывать несовместимые типы. Можно выполнять преобразование совместимых типов и все равно получить ошибку в runtime.

Type guards

В TypeScript есть другие способы преобразования типов. Один из таких способов - это использование type guard-ов. Рассмотрим пример:

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

Выражение "click" in pointer является type guard-ом. Оно в runtime проверяет, что поле "click" содержится внутри объекта pointer. Если это выражение возвращает true, то внутри блока if гарантируется, что "click" в объекте присутствует, а значит и с точки зрения типов у pointer должно быть поле click. Гарантируется также, что в блоке else у объекта pointer нет поля click.

На самом деле в коде выше описан особый случай для type guard-а in. pointer имеет тип Mouse | Keyboard, а оба интерфейса имеет разные названия полей. Соответственно, внутри блока if, TypeScript не просто убедился, что у pointer есть click. Он также определил, что click есть только у Mouse, а значит значения типа Keyboard не могут попасть в блок if, поэтому pointer внутри блока if имеет тип Mouse. Верно и обратное утверждение. Если "click" in pointer возвращает false, то в блоке else объект pointer не может иметь тип Mouse, потому что для этого у него должно быть поле click. Поэтому в блоке else у pointer тип Keyboard.

Операторы === и !== в TypeScript тоже являются type guard-ами.

При помощи === можно определять одно значение из множества других значений, а также исключать это значения из множества при помощи !== или блока else.

При помощи ===/!== и оператора typeof можно определять тип значения, если изначально тип этого значения состоял из объединения примитивов.

Оператор instanceof также является type guard-ом.

В TypeScript можно определить функции, которые являются tpye guard-ами. Для этого используется синтаксическая конструкция {paramter} is {type}

В коде выше объявлена функция isNumber возвращаемое значение которой a is number. По сути это boolean, но здесь также указано, что если возвращаемое значение true, то значение в параметра a - это number. Таким образом, функция isNumber становится type guard-ом.

В TypeScript уже есть встроенные функции type guard-ы, например Array.isArray.

Pattern Matching

Pattern Matching еще один подход, который позволяет преобразовать один тип во второй. Допустим, есть такой код:

Тип поля q.kind TypeScript выводит как "a" | "b". Можно воспользоваться оператором ===, для того, чтобы преобразовать тип A | B в A или B.

В такой ситуации можно воспользоваться оператором switch по полю q.kind:

В этом случае TypeScript также выводит тип внутри блоков case. Этот подход называется pattern matching.

Enum

Enum (enumeration - перечисление) - это еще одна синтаксическая конструкция TypeScript, которая пригождается в реальных задачах. Она не связан с преобразованием типов. Ее использования чаще всего связаны с взаимодействием с сервером.

Во многих языках программирования, на которых разрабатываются сервера, есть возможность объявить специальную структуру данных, которая хранит в себе именованный набор констант. Например, если мы разрабатываем какое-нибудь консольное приложение для отрисовки 2D графики в черно-белом формате, то для определения цвета можно использовать булевой флаг isBlack. Но когда цветов становится больше чем 2, использовать булевой флаг не получится. Можно использовать числовые значения от 0 до n. Пример сигнатуры функции, которая рисует пиксель по заданным координатам, на TypeScript может выглядеть так:

У такого подхода есть несколько проблема. Например, если поддерживается всего 4 цвета: 0, 1, 2 и 3, то в данном случаи типизация позволяет передавать любое число в качестве параметра color. Это может привести к проблемам в runtime. Конкретно в TypeScript для решения этой проблемы можно использовать специальный тип - объединение числовых литералов 0 | 1 | 2 | 3. Но очень многие языки программирования не поддерживают литеральные типы. Более того, при таком подходе становится сложно читать код:

В качестве цвета можно использовать его название в виде строки. Но тогда не решается проблема с передачей не валидных данных.

В разных языках программирования (в частности в TypeScript) есть enum-ы или подобные структуры данных. В TypeScript он выглядит так:

Enum в TypeScript - это объект, ключами которого являются строчные значения, указанные в enum-е, а значениями - числа или строки. Для кода выше TypeScript при компиляции сгенерирует следующий JavaScript:

Если ключам enum-а явно не передавать значения, то им присвоятся значения начиная с 0 сверху вниз увеличиваясь на 1. Любому элементу можно задать значение явно. Тогда у следующего элемента значение будет увеличено на 1 от предыдущего (если оно явно не указано).

Значения enum-а могут быть и строки. Но в таком случаи у всех ключей enum-а значения должны быть указаны явно, иначе будет ошибка компиляции. В одном enum-е могут быть одновременно и строчные и числовые значения.

Задания

Last updated

Was this helpful?