TypeScript: Типы данных, аннотация и синонимы

В этой статье мы рассмотрим распространённые типы значений. Это не исчерпывающий список, и в следующих статьях мы рассмотрим другие способы именования и использования типов.

Аннотации типов

TypeScript — типизированный язык, в котором можно указать тип переменных, параметров функций и свойств объектов. Мы можем указать тип, используя :тип после имени переменной, параметра или свойства. После двоеточия может быть пробел. TypeScript включает в себя все примитивные типы JavaScript - number, string и boolean.

В следующем примере объявляются переменные с разными типами данных:

var age: number = 32; // number variable
var name: string = "John";// string variable
var isUpdated: boolean = true;// Boolean variable

В приведённом выше примере каждая переменная объявлена со своим типом данных. Это аннотация типов. Вы не можете изменить значение, используя другой тип данных, отличный от объявленного типа данных переменной. Если вы попытаете сделать это, компилятор TypeScript покажет ошибку. Это помогает отлавливать ошибки JavaScript. Например, если вы присвоите переменной age строку или число переменной name в приведённом выше примере, то это выдаст ошибку.

Аннотации типов используются для принудительной проверки типов. В TypeScript необязательно использовать аннотации типов. Однако аннотации типов помогают компилятору проверять типы и помогают избежать ошибок связанных с типом данных. Это также хороший способ написания кода для облегчения его чтение и сопровождения будущими разработчиками, работающими над вашим кодом.

Мы по-прежнему можем использовать способ объявления переменных в JavaScript и позволять компилятору TypeScript выводить тип самостоятельно. Узнать больше о выведении типа компилятором можно в статье TypeScript: Выведение типа.

В следующем примере демонстрируется аннотация типа параметров:

function display(id:number, name:string)
{
console.log("Id = " + id + ", Name = " + name);
}

Аналогично мы можем объявить объект со встроенными аннотациями для каждого свойства.

var employee : {
id: number;
name: string;
};

employee = {
id: 100,
name : "John"
}

Мы объявляем объект employee с двумя свойствами id и name с типами данных number и string соответственно.

Если вы попытаетесь присвоить id строковое значение, то компилятор TypeScript выдаст следующую ошибку:

error TS2322: Type '{ id: string; name: string; }' is not assignable to type
'{ id:number; name: string; }'.Types of property 'id' are incompatible.
Type 'string' is not assignable to type 'number'.

Примитивы: number, string и boolean

В JavaScript есть три часто используемых примитива: number, string и boolean. У каждого есть соответствующий тип в TypeScript. Как и следовало ожидать у них те же самые имена, которые мы бы увидели воспользовавшись оператором typeof в JavaScript для значений этих типов.

number

Все числа хранятся в виде чисел с плавающей запятой. Эти числа могут быть десятичными, шестнадцатеричными, восьмеричными или двоичными.

let first:number = 123;       // десятичное
let second: number = 0x37CF; // шестнадцатеричное
let third:number=0o377 ; // восьмеричное
let fourth: number = 0b111001;// двоичное

console.log(first); // 123
console.log(second); // 14287
console.log(third); // 255
console.log(fourth); // 57

В приведённом выше примере, let first:number = 123; — хранит положительное целое число, как number. let second: number = 0x37CF; — шестнадцатеричное число, как число равное 14287. Когда вы выводите это число в консоли, оно выводится в виде десятичного числа эквивалентного заданному шестнадцатеричному. let third:number=0377; — хранит восьмеричное число, эквивалентное 255. И let fourth: number = 0b111001; — хранит двоичное число эквивалентное 57.

string

string — ещё один примитивный тип данных, который используется для хранения текстовых данных. Строковые значения заключаются в одинарные или двойные кавычки.

let employeeName:string = 'John Smith';
//Или
let employeeName:string = "John Smith";

Шаблонные строки

Начиная с версии TypeScript 1.4, в TypeScript включена поддержка шаблонных строк ES6. Шаблонные строки используются для встраивания выражения в строки.


let employeeName:string = "John Smith";
let employeeDept:string = "Finance";

// Pre-ES6
let employeeDesc1: string = employeeName + " works in the " + employeeDept + " department.";

// Post-ES6
let employeeDesc2: string = `${employeeName} works in the ${employeeDept} department.`;

console.log(employeeDesc1);//John Smith works in the Finance department.
console.log(employeeDesc2);//John Smith works in the Finance department.

Вместо записи строки, представляющей комбинацию текст и переменных с конкатенацией, мы можем использовать один оператор с обратными кавычками `. Значения переменных записываются как ${}. Используя шаблонные строки проще встраивать выражения, а также менее утомительно писать длинные текстовые строки.

boolean

Логические значения поддерживаются как в JavaScript, так и в TypeScript и хранятся в виде значений true/false.

let isPresent:boolean = true;

Обратите внимание, что логическое Boolean отличается от boolean в нижнем регистре. Boolean — объектный тип, тогда как boolean — примитивный тип. Рекомендуется использовать примитивный тип boolean, хотя JavaScrip приводит объект к его примитивному типу, система типов TypeScript этого не делает. TypeScript рассматривает его как тип объекта.

Таким образом, вместо объявления типа в верхнем регистре function checkExistence(b: Boolean) следует использовать название логического типа в нижнем регистре function checkExistence(b: boolean).

Менее распространённые примитивы

Стоит упомянуть ещё два примитива в системе типов JavaScript. Хотя мы не будем сильно углубляться.

bigint

Начиная с ES2020 в JavaScript есть примитив используемый для очень больших чисел — BigInt:

// Создание bigint через функцию BigInt
const oneHundred: bigint = BigInt(100);

// Создание BigInt через литеральный синтаксис
const anotherHundred: bigint = 100n;

Узнать больше о BigInt можно в примечании к выпуску TypeScript 3.2.

symbol

В JavaScript есть примитив, используемый для создания глобальной уникальной ссылки с помощью функции Symbol():

const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
// Никогда не выполнится
}

Это условие всегда будет возвращать false, поскольку тип firstName и тип secondName не пересекаются.

Больше информации вы можете получить в справочном руководстве Symbol

Array

Массив (Array) — особый тип данных, который может последовательно хранить несколько значений разных типов данных с использованием специального синтаксиса.

TypeScript поддерживает массивы, как и JavaScript. Есть два способа объявления массива:

  1. Используя квадратные скобки. Этот метод похож на то, как вы объявляете массивы в JavaScript:

    let fruits: string[] = ['Apple', 'Orange', 'Banana'];
  2. Использование общего типа (дженерика) массива, Array<типЭлемента>:

    let fruits: Array<string> = ['Apple', 'Orange', 'Banana'];

Оба метода дают одинаковый результат.

Конечно, вы всегда можете инициализировать массив, как показано ниже, но вы не получите преимуществ системы типов TypeScript:

let arr = [1, 3, 'Apple', 'Orange', 'Banana', true, false];

Массивы могут содержать элементы любого типа данных, числа, строки или даже объекты.

Массивы могут быть объявлены и инициализированы раздельно.

let fruits: Array<string>;
fruits = ['Apple', 'Orange', 'Banana'];

let ids: Array<number>;
ids = [23, 34, 100, 124, 44];

Массив в TypeScript может содержать элементы разных типов данных, используя синтаксис общего типа (дженерик) массива:

let values: (string | number)[] = ['Apple', 2, 'Orange', 3, 4, 'Banana'];
// или
let values: Array<string | number> = ['Apple', 2, 'Orange', 3, 4, 'Banana'];

Tuple

TypeScript представил новый тип данных — Кортеж илиTuple. Кортеж может содержать два значения разных типов данных.

Объявление переменных типов number, string и tuple:

var empId: number = 1;
var empName: string = "Steve";

// Переменная типа tuple
var employee: [number, string] = [1, "Steve"];

В приведённом выше примере мы определили переменную empId как number и empName как значение типа string. Мы объявили и присвоили две переменных идентификатора и имени сотрудника. То же самое может быть достигнуто с использованием одной переменной типа кортеж (tuple). employee — переменная типа tuple с двумя значениями типа number и string. Таким образом устраняется необходимость объявлять две переменные разных типов.

Переменная типа tuple может включать множество значений разных типов:

var employee: [number, string] = [1, "Steve"];
var person: [number, string, boolean] = [1, "Steve", true];

var user: [number, string, boolean, number, string];// объявление переменной типа tuple
user = [1, "Steve", true, 20, "Admin"];// инициализация переменной типа tuple

Также вы можете объявить массив кортежей:

var employee: [number, string][];
employee = [[1, "Steve"], [2, "Bill"], [3, "Jeff"]];

TypeScript генерирует массив в JavaScript для переменной типа tuple. Например, var employee: [number, string] = [1, 'Steve'] будет скомпилировано как var employee = [1, "Steve"] в JavaScript.

Доступ к элементам tuple/кортежа

Доступ к элементам кортежа можно получить используя индекс, так же как и к массиву. Индекс начинается с нуля.

var employee: [number, string] = [1, "Steve"];
employee[0]; // 1
employee[1]; // "Steve"

Добавление элементов в tuple/кортеж

Добавлять новые элементы в кортеж можно с помощью метода push().

var employee: [number, string] = [1, "Steve"];
employee.push(2, "Bill");
console.log(employee); // [1, 'Steve', 2, 'Bill']

В приведённом выше примере действие разрешено потому что мы добавляем валидные для кортежа employee значения типа number и string.

Теперь попробуем добавить в кортеж employee значение типа boolean:

employee.push(true)

Это вызовет следующую ошибку:

test.ts(4,15): error TS2345:
Argument of type 'true' is not assignable to parameter of type 'number | string'.

Мы получили сообщение об ошибке, говорящее, что добавление значения типа boolean к кортежу типа number | string недопустимо. Следовательно, кортеж объявленный как number | string может хранить только числовые и строковые значения.

Кортеж подобен массиву. Значит мы можем использовать для кортежа метода массива, такие как pop(), concat() и т.д.

var employee: [number, string] = [1, "Steve"];

// получение значение по индексу и выполнение операции
employee[1] = employee[1].concat(" Jobs");
console.log(employee); //Вывод: [1, 'Steve Jobs']

enum

Enum или перечисления — новый тип данных, поддерживаемый в TypeScript. Большинство объектно-ориентированных языков, таких как Java и С#, использую перечисления. Теперь это доступно и в TypeScript.

Перечисления позволяют объявлять набор именованных констант, то есть набор связанных значения, которые могут быть числовыми или строковыми значениями.

Существует три типа перечислений:

  1. Числовое перечисление
  2. Строковое перечисление
  3. Гетерогенное перечисление

Числовое Перечисление

Числовые перечисления основаны на числах, т.е. они хранят строковые значения в виде чисел.

Перечисления могут быть определены с помощью ключевого слова enum. Допустим, мы хотим сохранить набор типов печатных носителей. Соответствующее перечисление в TypeScript будет таким:

enum PrintMedia {
Newspaper,
Newsletter,
Magazine,
Book
}

В приведённом выше перечислении у нас задано перечисление PrintMedia. Перечисление имеет четыре значения: Newspaper, Newsletter, Magazine, и Book. Значения перечисления начинаются с нуля и увеличиваются на единицу для каждого элемента. Это будет представлено так:

Newspaper = 0
Newsletter = 1
Magazine = 2
Book = 3

Перечислениям всегда присваиваются числовые значения при сохранении. Первое значение всегда равно 0, а остальные значения увеличиваются на 1.

Мы можем самостоятельно инициализировать первое числовое значение. Например, мы можем записать то же перечисление так:

enum PrintMedia {
Newspaper = 1,
Newsletter,
Magazine,
Book
}

Первый элемент, Newspaper, инициализируется числовым значением 1. Остальные будут увеличены на 1 от числового значения первого элемента перечисления. Таким образом, в приведённом выше примере Newsletter будет иметь значение 2, Magazine3 и Book4.

Нет необходимости присваивать последовательные значения элементам перечисления. Они могут иметь любые значения.

enum PrintMedia {
Newspaper = 1,
Newsletter = 5,
Magazine = 5,
Book = 10
}

Перечисление можно использовать как параметр функции или возвращаемый тип:

enum PrintMedia {
Newspaper = 1,
Newsletter,
Magazine,
Book
}

function getMedia(mediaName: string): PrintMedia {
if ( mediaName === 'Forbes' || mediaName === 'Outlook') {
return PrintMedia.Magazine;
}
}

let mediaType: PrintMedia = getMedia('Forbes'); // возвращает Magazine

В приведённом выше примере мы объявили перечисление PrintMedia. Затем объявляем функцию getMedia(), которая принимает входной параметр mediaName типа string. Эта функция возвращает перечисление PrintMedia. В функции мы проверяем тип медиа. Название медиа совпадает с Forbes или Outlook, мы возвращаем элемент перечисления PrintMedia.Magazine.

Вычисляемые перечисления

Числовые перечисления могут включать элементы с вычисляемыми числовым значением. Значение элемента перечисления может быть либо константой, либо вычисляемым значением. Следующее перечисление включает элементы с вычисляемыми значениями:

enum PrintMedia {
Newspaper = 1,
Newsletter = getPrintMediaCode('newsletter'),
Magazine = Newsletter * 3,
Book = 10
}

function getPrintMediaCode(mediaName: string): number {
if (mediaName === 'newsletter') {
return 5;
}
}

PrintMedia.Newsletter; // возвращает 5
PrintMedia.Magazine; // возвращает 15

Когда перечисление включает в себя вычисляемые и константные элементы, тогда неинициализированные элементы перечисления должны быть либо первыми, либо после других инициализированных элементов с числовыми константами. Следующий пример выдаст ошибку:

enum PrintMedia {
Newsletter = getPrintMediaCode('newsletter'),
Newspaper, // Error: Enum member must have initializer
Book,
Magazine = Newsletter * 3,
}

Это перечисление должно быть объявлено так:

enum PrintMedia {
Newspaper,
Book,
Newsletter = getPrintMediaCode('newsletter'),
Magazine = Newsletter * 3
}
// или
enum PrintMedia {
Newsletter = getPrintMediaCode('newsletter'),
Magazine = Newsletter * 3,
Newspaper = 0,
Book,
}

Строковое перечисление

Строковые перечисления аналогичны числовым перечислениям, за исключением того, что значение перечисления инициализируются строковыми значениями, а не числовыми.

Преимущество строковых перечислений — лучшая читабельность. Если бы мы отлаживали программу, было бы легче читать строковые, а не числовые значения.

Рассмотрим тот же пример перечисления, что и ранее, но в виде строкового перечисления:

enum PrintMedia {
Newspaper = "NEWSPAPER",
Newsletter = "NEWSLETTER",
Magazine = "MAGAZINE",
Book = "BOOK"
}
// Доступ к строковому перечислению
PrintMedia.Newspaper; //возвращает NEWSPAPER
PrintMedia['Magazine'];//возвращает MAGAZINE

В приведённом выше примере мы определили строковое перечисление printMedia с теми же значениями, что и числовое перечисление выше, с той разницей, что эти значения инициализируются строковыми литералами. Разница между числовыми и строковыми перечислениями заключается в том, что числовые значения перечислений автоматически увеличиваются, в то время как строковые значения необходимо инициализировать индивидуально.

Гетерогенное перечисление

Гетерогенные перечисления — перечисления содержащие как строковые, так и числовые значения.

enum Status {
Active = 'ACTIVE',
Deactivate = 1,
Pending
}

Обратное сопоставление

Перечисление в TypeScript поддерживают обратное сопоставление/Reverse Mapping. Это означает, что мы можем получить доступ к значению элемента, а также имени элемента из его значения. Рассмотрим следующий пример:

enum PrintMedia {
Newspaper = 1,
Newsletter,
Magazine,
Book
}

PrintMedia.Magazine; // возвращает 3
PrintMedia["Magazine"];// возвращает 3
PrintMedia[3]; // возвращает Magazine

Как видно из приведённого примера, printMedia[3] возвращает имя своего элемента Magazine. Это происходит из-за обратного сопоставления. Давайте посмотрим, как TypeScript реализует обратное сопоставление:

enum PrintMedia {
Newspaper = 1,
Newsletter,
Magazine,
Book
}
console.log(PrintMedia)

Приведённый пример даст следующий вывод в консоли:

{
'1': 'Newspaper',
'2': 'Newsletter',
'3': 'Magazine',
'4': 'Book',
Newspaper: 1,
Newsletter: 2,
Magazine: 3,
Book: 4
}

Как вы видите, каждое значение перечисления появляется дважды во внутреннем объекте перечисления. Мы знаем, что числовые значения можно получить, используя соответствующие значения элемента перечисления. Но также верно и то, что элементы перечисления можно получить используя их значения. Это называется обратным сопоставлением.

TypeScript может скомпилировать приведённое выше перечисление в следующую функцию JavaScript:

var PrintMedia;
(function (PrintMedia) {
PrintMedia[PrintMedia["Newspaper"] = 1] = "Newspaper";
PrintMedia[PrintMedia["Newsletter"] = 2] = "Newsletter";
PrintMedia[PrintMedia["Magazine"] = 3] = "Magazine";
PrintMedia[PrintMedia["Book"] = 4] = "Book";
})(PrintMedia || (PrintMedia = {}));

PrintMedia — объект JavaScript включающий в себя как значение, так и имя в качестве свойств, поэтому перечисления TypeScript поддерживают обратное сопоставление.

Примечание: Обратное сопоставление не поддерживается для элементов строкового перечисления. Для гетерогенного перечисления обратное сопоставление поддерживается только для элементов типа number, но не для string.

Union/Объединение

TypeScript позволяет использовать более одного типа данных для переменной или параметра функции. Это называется объединением union.

(тип1 | тип2 | тип3 | .. | типN)

Рассмотрим пример использования типа union:

let code: (string | number);
code = 123; // OK
code = "ABC"; // OK
code = false; // Compiler Error

let empId: string | number;
empId = 111; // OK
empId = "E111"; // OK
empId = true; // Compiler Error

В приведённом примере переменная code имеет тип union, заданный с помощью (string | number). Вы можете присвоить ей значения типа number или string.

Параметр функции также может быть типа union:

function displayType(code: (string | number))
{
if(typeof(code) === "number")
console.log('Code is number.')
else if(typeof(code) === "string")
console.log('Code is string.')
}

displayType(123); // Вывод: Code is number.
displayType("ABC"); // Вывод: Code is string.
displayType(true); //Compiler Error: Argument of type 'true' is not assignable to a parameter of type string | number

В приведённом примере параметру code задан тип union. Таким образом, вы можете передавать либо строковое, либо числовое значение. Если вы передадите любой другой тип значения, например boolean, то компилятор выдаст ошибку.

any

В TypeScript есть проверка типов и проверка во время компиляции. Однако мы не всегда можем знать заранее тип некоторых переменных, особенно когда есть введённые пользователем значения из сторонних библиотек. В таких случаях нужно приложение, которое может работать с динамическим контентом. Здесь пригодится тип any.

let something: any = "Hello World!";
something = 23;
something = true;

Код из примера будет скомпилирован в следующий JavaScript код:

var something = "Hello World!";
something = 23;
something = true;

Подобным образом можно создать массив типа any[], если вы не уверены в типах значений, которые могут в нём содержаться:

let arr: any[] = ["John", 212, true];
arr.push("Smith");
console.log(arr); //Вывод: [ 'John', 212, true, 'Smith' ]

Этот пример будет скомпилирован в следующий JavaScript код:

var arr = ["John", 212, true];
arr.push("Smith");
console.log(arr);

void

Как и в языках, таких как Java, void используется там, где не данных. Например, если функция не возвращает никакого значения, можно указать тип возвращаемого значения void:

function sayHi(): void {
console.log('Hi!')
}

let speech: void = sayHi();
console.log(speech); //Вывод: undefined

Нет смысла присваивать тип void переменной, так как ей можно будет присвоить только null или undefined.

let nothing: void = undefined;
let num: void = 1; // Ошибка

never

TypeScript представил новый тип never указывающий значения, которые никогда не будут встречаться.

Тип never используется, когда вы уверены, что что-то никогда не произойдёт. Например, вы пишите функцию, которая не возвращается к своей конечной точке и всегда выдаёт исключение:

function throwError(errorMsg: string): never {
throw new Error(errorMsg);
}

function keepProcessing(): never {
while (true) {
console.log('I always does something and never ends.')
}
}

В приведённом примере функция throwError() выдаёт ошибку, а функция keepProcessing() всегда выполняется и никогда не достигнет конечной точки, потому что задан бесконечный цикл while (true). Таким образом, тип never используется для указания значения, которое никогда не возникнет и не будет возвращено функцией.

Разница между never и void

Тип void может иметь в качестве значения null или undefined, в то время как never не может иметь никакого значения.

let something: void = null;
let nothing: never = null; // Error: Type 'null' is not assignable to type 'never'

В TypeScript функция, которая не возвращает значения, на самом деле возвращает undefined. Рассмотрим пример:

function sayHi(): void {
console.log('Hi!')
}

let speech: void = sayHi();
console.log(speech); // undefined

Как вы можете видеть в примере, speech()undefined, потому что функция sayHi() возвращает undefined, даже если возвращаемый тип void. Если вы используете тип never, speech: never выдаст ошибку времени компиляции, так как тип void не может быть назначен переменной типа never.

Псевдоним типа

Мы использовали типы объектов и типы объединения задавая их непосредственно в аннотациях типов. Это удобно, но часто хочется использовать один и тот же тип более одного раза и ссылаться на него по одному имени.

Псевдоним типа — это именно то, имя для любого типа. Синтаксис псевдонима типа:

type Point = {
x: number;
y: number;
};

function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}

printCoord({ x: 100, y: 100 });

На самом деле вы можете использовать псевдоним типа, чтобы дать имя вообще любому типу, а не только объекту. Например, псевдоним типа можно использовать для задания типа объединению:

type ID = number | string;

Обратите внимание, что псевдонимы — это всего лишь псевдонимы — вы не можете использовать их для создания разных версий одного и того же типа. Когда вы используете псевдоним, это точно так же, как если бы вы написали тип с псевдонимом. Другими словами, этот код может выглядеть недопустимым, но в соответствии с TypeScript это нормально, потому что оба типа являются псевдонимами для одного и того же типа.

type UserInputSanitizedString = string;

function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}

// Создание очищенного ввода
let userInput = sanitizeInput(getInput());

// Однако, всё ещй можно переназначить строку
userInput = "new input";

Дополнительные материалы

Предыдущая Статья

Laravel: Параметры маршрута из нескольких слов

Следующая Статья

Laravel: Что такое Collection / Коллекции