Конструкции языка
Приведение типов
До этого момента мы ни разу не говорили о том, как изменять тип значения. В 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 undefined
Type 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?