Продолжаем серию статей, где кратко рассказываем о технологиях, которые используются в современной фронт-энд разработке.

Почему Typescript? Typescript - статически-типизируемый язык программирования. Он нужен для того чтобы удобнее отлавливать ошибки ещё на процессе их компиляции. TypeScript компилируется в ванильный JavaScript. В TypeScript есть типы данных и статическая система типизации, поэтому понимание кода и количество ошибок связанных с динамической типизацией станет намного меньше.

На официальном сайте TypeScript есть специальная страница Playground, которая поможет вам эксперементировать с языком по мере изучения и чтения данной статьи.

Компиляция файлов и их запуск

Для того чтобы создать свой первый TypeScript файл достаточно просто создать файл с форматом .ts. В нём мы будем писать TypeScript код, который будет компилироваться в ванильный JavaScript.

Также нам нужен компилятор TypeScript. Его можно скачать с помощью данной команды:

npm i -D typescript

Если вы хотите установить компилятор глобально, то вам понадобится следующая команда:

npm i -g typescript

Давайте напишем простой скрипт на TS, который просто будет выводить строку (не пугайтесь, пока что ничего не будет понятно):

const str: string = 'Hello, World!'; // Объявление и инициализация
console.log(str); // Вывод

Как вы можете увидеть синтаксис TypeScript практически идентичен синтаксису из JS. В данном случае мы объявили переменную с типом данных string через : (), а затем инициализировали значение 'Hello, World!'.

Для того чтобы скомпилировать файл (допустим string.ts) с помощью команды tsc <имя файла>.

Типы данных

Булевый тип

Для того чтобы объявить переменную логического типа нам нужно указать : boolean после двоеточего:

const isLoading: boolean = false;

Числовой тип

const int: number = 40;
const int2: pi = 3.1415;

Если мы попробуем присвоить другой тип данной переменной (напр.):

const int: number = 'string';

То, TypeScript выдаст нам ошибку:

Ошибка на сайте песочницы TypeScript
Ошибка на сайте песочницы TypeScript

Может случиться так, что наша переменная должна быть не строго типизированна, тогда к нам на помощь идёт ключевое слово any:

let variable: any = 'string';
variable = 3;
console.log(variable);

null и undefined

null и undefined были представлены в JavaScript ещё давно. Они есть и в TypeScript.

Из MDN:

Значение null представляет отсутствие какого-либо объектного значения. В JavaScript, null является примитивом, и в контексте логических операций, рассматривается как ложное (falsy).

null является определённым значением отсутствия объекта, тогда как undefined обозначает неопределённость. Например: 

var element;
// значение переменной element до её инициализации не определённо: undefined

element = document.getElementById('not-exists');
// здесь при попытке получения несуществующего элемента, метод getElementById возвращает null
// переменная element теперь инициализирована значением null, её значение определено

undefined является свойством глобального объекта, то есть, это переменная в глобальной области видимости. Начальным значением undefined является примитивное значение undefined.

В современных браузерах (JavaScript 1.8.5 / Firefox 4+), undefined является ненастраиваемым и незаписываемым свойством, в соответствии со спецификацией ECMAScript 5. Даже когда это не так, избегайте его переопределения.

Переменная, не имеющая присвоенного значения, обладает типом undefined. Также undefined возвращают метод или инструкция, если переменная, участвующая в вычислениях, не имеет присвоенного значения. Функция возвращает undefined, если она не возвращает какого-либо значения.

Поскольку undefined не является зарезервированным словом (en-US), он может использоваться в качестве идентификатора (имени переменной) в любой области видимости, за исключением глобальной.

Типы данных можно не указывать сразу

Если вы напишете следующий код:

let string = 'string';
let num = 3;

То, компилятор TypeScript сразу определит статический тип переменной. То есть код вверху эквивалентен данному коду:

let string: string = 'string';
let num: number = 3;

Свой тип данных

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

type login = string;
type password = string;

let login: login = 'user';
const simplePassword: password = 'password0123456789';

Тут используется ключевое слово type, которое нужно для создания новых типов данных. Данные типы используются в основном для того чтобы упростить разработку, однако, иногда бывает что вы не знаете какой тип данных будет использоваться для переменной (например тот же ID, который может быть либо строкой либо числом). Для этих случаев существует подобный синтаксис:

type id = string | number;
const id1: id = 'ididididi';
const id2: id = 123;

Массивы

Массивы тоже строго типизированы. Если раньше можно было поместить в массив всё что угодно, то теперь только данные одного типа:

const arr: number[] = [1,2,4,8,16];
// или
const arr2: Array<Number> = [16, 8, 4, 2, 1];
// Данная запись называется generic (или обобщенный тип)

С помощью подобной записи можно создать массив из любых примитивных типов данных:

const arr3: Array<String> = ['st', 'ri', 'ng'];

Списки

Списки (Tuples) предоставлены нам для того, чтобы мы могли чередовать типы данных в массивах. Если вам нужен массив из имён и возрастов, то вы можете сделать его вот так (на практике лучше такого не делать):

const nameAndNumber: [string, number] = ['Daniil', 19];

Функции

Функции в TypeScript имеют запись немного схожую с объявлением переменных:

function sayMessage(message: string): void {
  console.log();
}

Что за void? Почему : стоит рядом с параметром? Сейчас разберемся.

Функция в TypeScript объявляется как и в обычном JavaScript, однако, рядом с параметром мы явно указываем тип данных, который назначен этому параметру (у нас функция первым параметром принимает строковый тип данных), а после объявления самой функции мы указываем тип данных, который данная функция вернет. В нашем случае это void. Данное ключевое слово вы могли видеть в заглушках ссылок (javascript:void(0)). Данный тип данных означает что функция ничего не возвращает.

Разбор кода

Попробуйте разобрать данный код в песочнице:

function sayTypeOfName(name: string): void {
  console.log(typeof name);
}

sayTypeOfName();
console.log(typeof sayTypeOfName);
console.log(typeof sayTypeOfName());

Как вы думали что выведет данный код? Что он вывел на самом деле? Не забыли ли вы, что TypeScript просто транслирует код в ванильный JavaScript и не добавляет ничего нового в получаемый код?

Есть ещё один тип данных, который подходит только функциям: never.

never используется для тех функций, которые выполняются вечно или возвращают ошибку:

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

Вот что выведет данный код:

Вывод из песочницы
Вывод из песочницы

Функция, которая будет что-то возвращать будет выглядеть так:

function addTwo(n: number): number {
	return n + 2;
}

Интерфейсы

Интерфейсы создаются для того, чтобы описывать какие поля, функции, методы имеются в структуре данных.

Перевод из официальной документации TypeScript

В JavaScript, мы всё время пользуемся объектами для передачи данных. В TypeScript, мы представляем объекты через объектные типы.

Как мы уже видели они могут быть анонимными:

function greet(person: { name: string; age: number }) {
	return "Hello " + person.name;
}

или все свойства могут быть пропущены через интерфейс:

interface IPerson{
	name: string;
  age: number;
}

function greet(person: IPerson) {  
	return "Hello " + person.name;
}

Объявить интерфейс можно с помощью ключевого слова interface:

interface IPerson {
    name: string;
    surname: string;
    age: number;
    readonly id: number | string;
    work?: string;
  	contact: {
  		email: string;
  		phoneNumber?: string;
		}
}

Тут мы видим уже знакомую нам конструкцию с оператором | (или). Знак вопроса перед двоеточием говорит о том, что данное свойство - необязательное (то есть его можно указывать, а можно и пропустить). Ключевое слово readonly говорит о том, что данное свойство объекта нельзя будет изменить.

Для того чтобы объявить объект используя интерфейс нужно просто указать название интерфейса как тип данных:

const Daniil : IPerson = {
	name: 'Daniil',
  surname: 'Shilo',
  age: 19,
  id: 0,
  work: 'Front-end Developer',
  contact: {
    email: 'daniilshilo.developer@gmail.com'
  }
}

Как вы видите поле work не является обязательным но, я все равно его ввёл. Поле contact.phoneNumber является необязательным и соответственно тут я его не оставил (хотя если бы указал, то TS не указал бы ошибки).

Разбор кода

Попробуйте найти в данном коде ошибку и исправить.

Подсказка: в песочнице справа есть окно с ошибками, можно отладить код благодаря тому, что TS сразу показывает какие ошибки есть в коде

Ссылка на задание

Также с помощью интерфейсов можно описывать классы:

interface IEmployee {
    id: number;
    name: string;
    getSalary:(empCode: number) => string;
}

class Employee implements IEmployee { 
    id: number;
    name: string;
    constructor(code: number, name: string) { 
        this.id = code;
        this.name = name;
    }

    getSalary(empCode:number):string { 
        return '20000$';
    }
}

let emp = new Employee(1, "John");
console.log(emp);

Если у нас есть кастомные свойства, которые будут в объекте то можно записать их с помощью следующего синтаксиса:

interface IStyles {
    [style: string]: string;
}

const documentStyles: IStyles = {
    border: 'none',
    color: 'white',
    backgroundColor: 'black'
}
Не делайте так

Если вам нужно записать множество ключей в массив с одинаковым типом значений, то не нужно перечислять каждый ключ:

interface IStyles {
    border: string;
    color: string;
    backgroundColor: string;
}

const documentStyles: IStyles = {
    border: 'none',
    color: 'white',
    backgroundColor: 'black'
}

Лучше используйте метод записи, который представлен над данным спойлером.

Наследование интерфейсов

Наследование интерфейсов очень похоже на наследование классов в JavaScript. В наследовании классов применяется ключевое слово extends тут оно применяется тоже. Возьмем наш интерфейс Person и добавим к нему пару полей, создадим интерфейс Employee:

interface Person {
    name: string;
    surname: string;
    age: number;
    readonly id: number | string;
    work?: string;
}

interface Employee extends Person {
    work: string;
    yearsOfExperince: number;
    sayName: () => void;
}

const Daniil: Employee = {
    name: 'Daniil',
    surname: 'Shilo',
    age: 19,
    id: 0,
    work: 'Front-End Developer',
    yearsOfExperince: 2,
    sayName: function() {
        console.log(this.name);
    }
};

Daniil.sayName();

Как мы видим тут мы объявили поле work обязательным, а также добавили новое поле yearsOfExperience и функцию, которая возвращает тип void.

Посмотреть на работу данного кода можно здесь.

Хорошей практикой является добавлять букву I в начале названия интерфейса, для того чтобы точно указать, что это интерфейс:

  • I_ID

  • IPerson

  • IDog

Enums

Enum (в переводе с англ.) - перечисление.

Из официальной документации (прим. пер.)

Перечисление это одна из фич TypeScript, которая не касается типизации в JavaScript.

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

Использование перечислений может упростить документирование намерений разработчика или создать набор частных случаев, которые будут описаны в перечислении. TypeScript предоставляет числовые и строковые перечисления.

Пример применения перечислений:

// Объявление перечисления
enum Names {
    Daniil,
    Alex,
    John
}

// Нахождения индекса имени в перечислении
const INDEX_OF_NAME: number = Names.Daniil;
// Нахождение значения по индексу имени
const REVERSE_NAME = Names[INDEX_OF_NAME];

console.log(INDEX_OF_NAME, REVERSE_NAME);

Посмотреть на данный код в действии можно здесь.

В документации упоминалось о строковых перечислениях, поэтому я покажу их тут тоже:

enum SocialLinks {
  Instagram = 'https://www.instagram.com/persik_danya/',
  Telegram = 'https://t.me/developer_log',
  Vk = 'https://vk.com/daniilshilo_developer'
}

Посмотреть на строковые Enums можно здесь.

Классы

Классы в TypeScript работают как и в JavaScript:

class Animal {
    /* При сборке компилятор будет проверять
    	не изменилось ли данное значение */
    readonly name: string;
    constructor(name: string) {
        this.name = name;
    }

    saySomething(): void {
        console.log('mryaow');
    }
}

const cat = new Animal('cat');
cat.saySomething();

Код вверху эквивалентен данному коду в JavaScript:

"use strict";
class Animal {
    constructor(name) {
        this.name = name;
    }
    saySomething() {
        console.log('mryaow');
    }
}
const cat = new Animal('cat');
cat.saySomething();

Однако, все равно можно увидеть некоторые особенности:

  • Мы используем специальную запись вне конструктора, для того чтобы явно указать тип свойства класса

  • Мы можем указать возвращаемое значение методов класса

Хорошей практикой является указание типов свойств до конструктора и других методов.

Модификаторы полей

Если вы когда-то программировали на C++, Java или C#, то вы уже знаете что такое. Остальным же советую читать статью дальше.

Существует три главных модификатора:

  • public - значение по умолчанию у всех методов и свойств. Данные методы и свойства можно вызывать и менять вне класса

  • protected - Методы и свойства с этим модификатором наследуются из класса в класс, однако, их нельзя менять извне класса

  • private - Данные методы и свойства не наследуются, а также их нельзя изменять вне класса. Они доступы только в классе, в котором были объявлены.

Пример кода на TypeScript:

class Animal {

    readonly name: string;
    constructor(name: string) {
        this.name = name;
        this.saySomething();
    }

    protected saySomething(): void {
        console.log('mryaow');
    }

    private yearsOld: number = 5;

}

class Cat extends Animal {
    constructor(name: string) {
        super(name);
    }
}

const animal = new Animal('Not a cat');
const cat = new Cat('cat');
console.dir(animal);
console.dir(cat);

try {
    cat.saySomething();
} catch(err) {
    console.error(err);
}

Сам код не выдаст никакой ошибки, однако, вы просто не сможете его скомпилировать, так как вызываете метод, который модифицирован как protected.

Что говорит песочница по этому поводу
Что говорит песочница по этому поводу

Абстрактные классы

Абстрактные классы нужны для того, чтобы их дочерние классы имели какие-то методы или свойства:

// Говорит о том, что у дочернего класса должны быть
// данные свойства и методы

abstract class People {
    abstract shoutSomething(): void;
    abstract age: number;
}

// Дочерний класс
class Daniil extends People {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        super();
        this.name = name;
        this.age = age;
        this.shoutSomething();
    }

    private shoutSomething() : void {
        console.log('Yeah!');
    }
}

const me = new Daniil('Daniil', 19);

А что творится в JavaScript? А вот что:

"use strict";
class People {
}
class Daniil extends People {
    constructor(name, age) {
        super();
        this.name = name;
        this.age = age;
        this.shoutSomething();
    }
    shoutSomething() {
        console.log('Yeah!');
    }
}
const me = new Daniil('Daniil', 19);

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

Поиграться с данным кодом можно здесь.

Обобщения (Дженерики)

Представьте что нам нужно удалить последний элемент из массива с помощью нашей собственной функции. Как указать тип массива у такой функции?

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

const nums: Array<number> = [1, 2, 3];

Функция с дженериком будет выглядеть так:

// Использование дженерика при инициализации массива
const nums: Array<number> = [1, 2, 3];
const strings: string[] = ['1', '2', '3'];
console.log(nums, strings);

// Использование дженерика для объявления функции,
// которая будет проглатывать любой массив
function pop <T>(arr: Array<T>): Array<T> {
    arr.pop();
    return arr;
}

console.log(pop(nums));
console.log(pop(strings));

Тут <T> прячет в себе тип массива, который мы будем передавать.

В завершение

Если вам понравилась данная статья, то вы можете подписаться на мой канал в телеграме, там много интересного.

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

Спасибо что прочитали статью, хорошего вам дня и продуктивной недели.

Комментарии (17)