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

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

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