Конструкции языка
Приведение типов
До этого момента мы ни разу не говорили о том, как изменять тип значения. В 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.
const a = 10;
const b = a as string; //Error: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.Однако это ограничение можно обойти, сделав сначала каст до any, а потом до нужного типа.
const a = 10;
const b = a as any as string;Иногда преобразование в несовместимый тип приходится делать. Но кастом через any не стоит злоупотреблять. Использование подобной конструкции может вызвать проблемы в runtime. После приведения типов, TypeScript использует для компиляции приведенный тип. Однако значение, тип которого был изменен (особенно через as any as) может быть не совместимо с операциями, которые можно делать с приведенным типом. Например, не иметь каких то методов. В таком случаи ошибка возникнет в runtime, а не на этапе компиляции.
const a = 15
const b = a as any as string;
b.slice(1); //Runtime Error: Uncaught TypeError: b.slice is not a functionДля того, чтобы сломать runtime при помощи каста, необязательно преобразовывать несовместимые типы. Можно выполнять преобразование совместимых типов и все равно получить ошибку в runtime.
function getValue(index: 0 | 1) {
const tupple: [number, number] = [1, 1];
return tupple[index];
}
const a = 15;
getValue(a as 0 | 1).toString() //Runtime Error: Uncaught TypeError: Cannot read property 'toString' of undefinedType guards
В TypeScript есть другие способы преобразования типов. Один из таких способов - это использование type guard-ов. Рассмотрим пример:
interface Keyboard {
pressKey: (key: number) => void;
}
interface Mouse {
click: (button: "left" | "right") => void;
}
function makeAction(pointer: Mouse | Keyboard) {
...
}В коде выше объявлено интерфейсы клавиатуры и мыши. Также объявлена функция, которая принимает объект совместимый с типом клавиатуры или типом мыши. Далее нужно написать код, который проверит, какой объект был передан в функцию и вызовет соответствующий метод.
function makeAction(pointer: Mouse | Keyboard) {
if ("click" in pointer) {
pointer.click("left")
} else {
pointer.pressKey(85)
}
}Выражение "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-ами.
function foo(a: number) {
if (a === 1) {
a //type: 1
}
if (a === 1 || a === 2) {
a //type: 1 | 2
}
a //type: number
}При помощи === можно определять одно значение из множества других значений, а также исключать это значения из множества при помощи !== или блока else.
function foo(a: "lalaka" | "malaka" | "palaka") {
if (a === "lalaka") {
a //type: "lalaka"
} else {
a //type: "malaka" | "palaka"
if (a !== "palaka") {
a //type: "malaka"
} else {
a //type: "palaka"
}
}
}При помощи ===/!== и оператора typeof можно определять тип значения, если изначально тип этого значения состоял из объединения примитивов.
function foo(a: string | number | boolean) {
if (typeof a === "string") {
a //type: string
} else {
a //type: number | boolean
}
}Оператор instanceof также является type guard-ом.
class A {
}
class B {
a: number = 10;
}
function foo(a: A) {
if (a instanceof B) {
const q = a.a;
}
}В TypeScript можно определить функции, которые являются tpye guard-ами. Для этого используется синтаксическая конструкция {paramter} is {type}
function isNumber(a: any): a is number {
return typeof a === "number"
}
function foo(a: string | number) {
if (isNumber(a)) {
a //type: number
}
}В коде выше объявлена функция isNumber возвращаемое значение которой a is number. По сути это boolean, но здесь также указано, что если возвращаемое значение true, то значение в параметра a - это number. Таким образом, функция isNumber становится type guard-ом.
В TypeScript уже есть встроенные функции type guard-ы, например Array.isArray.
Pattern Matching
Pattern Matching еще один подход, который позволяет преобразовать один тип во второй. Допустим, есть такой код:
interface A {
kind: "a",
do: (a: string) => boolean;
}
interface B {
kind: "b",
do: (b: number) => boolean;
}
function foo(q: A | B): boolean {
q.kind //type: "a" | "b"
}Тип поля q.kind TypeScript выводит как "a" | "b". Можно воспользоваться оператором ===, для того, чтобы преобразовать тип A | B в A или B.
function foo(q: A | B): boolean {
if (q.kind === "a") {
return q.do("lalaka");
} else {
return q.do(15);
}
}В такой ситуации можно воспользоваться оператором switch по полю q.kind:
function foo(q: A | B): boolean {
switch (q.kind) {
case "a":
return q.do("lalaka");
case "b":
return q.do(15);
}
}В этом случае TypeScript также выводит тип внутри блоков case. Этот подход называется pattern matching.
Enum
Enum (enumeration - перечисление) - это еще одна синтаксическая конструкция TypeScript, которая пригождается в реальных задачах. Она не связан с преобразованием типов. Ее использования чаще всего связаны с взаимодействием с сервером.
Во многих языках программирования, на которых разрабатываются сервера, есть возможность объявить специальную структуру данных, которая хранит в себе именованный набор констант. Например, если мы разрабатываем какое-нибудь консольное приложение для отрисовки 2D графики в черно-белом формате, то для определения цвета можно использовать булевой флаг isBlack. Но когда цветов становится больше чем 2, использовать булевой флаг не получится. Можно использовать числовые значения от 0 до n. Пример сигнатуры функции, которая рисует пиксель по заданным координатам, на TypeScript может выглядеть так:
function printPixel(x: number, y: number, color: number) {
}У такого подхода есть несколько проблема. Например, если поддерживается всего 4 цвета: 0, 1, 2 и 3, то в данном случаи типизация позволяет передавать любое число в качестве параметра color. Это может привести к проблемам в runtime. Конкретно в TypeScript для решения этой проблемы можно использовать специальный тип - объединение числовых литералов 0 | 1 | 2 | 3. Но очень многие языки программирования не поддерживают литеральные типы. Более того, при таком подходе становится сложно читать код:
printPixel(0, 0, 0);
printPixel(0, 1, 1);
printPixel(1, 0, 1);
printPixel(1, 1, 2);В качестве цвета можно использовать его название в виде строки. Но тогда не решается проблема с передачей не валидных данных.
В разных языках программирования (в частности в TypeScript) есть enum-ы или подобные структуры данных. В TypeScript он выглядит так:
enum Color {
Black,
White,
Red,
Green
}
const color = Color.Black;Enum в TypeScript - это объект, ключами которого являются строчные значения, указанные в enum-е, а значениями - числа или строки. Для кода выше TypeScript при компиляции сгенерирует следующий JavaScript:
var Color;
(function (Color) {
Color[Color["Black"] = 0] = "Black";
Color[Color["White"] = 1] = "White";
Color[Color["Red"] = 2] = "Red";
Color[Color["Green"] = 3] = "Green";
})(Color || (Color = {}));
const color = Color.Black;Если ключам enum-а явно не передавать значения, то им присвоятся значения начиная с 0 сверху вниз увеличиваясь на 1. Любому элементу можно задать значение явно. Тогда у следующего элемента значение будет увеличено на 1 от предыдущего (если оно явно не указано).
enum Color {
Black = 3,
White,
Red = 10,
Green
}
const black: 3 = Color.Black;
const white: 4 = Color.White;
const red: 10 = Color.Red;
const green: 11 = Color.Green;Значения enum-а могут быть и строки. Но в таком случаи у всех ключей enum-а значения должны быть указаны явно, иначе будет ошибка компиляции. В одном enum-е могут быть одновременно и строчные и числовые значения.
enum Color {
Black = "black",
White = "white",
Red = "red",
Green = "green"
}
enum WrongColor {
Black = "black",
White //Error: Enum member must have initializer.
}Задания
Last updated
Was this helpful?