К списку уроков

Урок 8. TypeScript.Типизация.

Онлайн, 21 октября (вторник), 15:15

На данном уроке мы с вами поговорим о языке TypeScript, зачем он нужен и как с ним работать

Презентация тык.

Материалы

Этих 2 ссылок хватит с головой, чтобы познать ts в полной мере

Мощное видео

Дока

TypeScript представляет язык программирования на основе JavaScript.

Казалось бы, зачем нужен еще один язык программирования для клиентской стороны в среде Web, если со всей той же самой работой прекрасно справляется и традиционный JavaScript, который используется практически на каждом сайте, которым владеет множество разработчиков и поддержка которого в сообществе программистов довольно высока. Но TypeScript это не просто новый JavaScript.

Во-первых, следует отметить, что TypeScript - это строго типизированный и компилируемый язык, чем, возможно, будет ближе к программистам Java, C# и других строго типизированных языков. Хотя на выходе компилятор создает все тот же JavaScript, который затем исполняется браузером. Однако строгая типизация уменьшает количество потенциальных ошибок, которые могли бы возникнуть при разработке на JavaScript.

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

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

В-четвертых, TypeScript развивается как opensource-проект и, как и многие проекты, хостится на гитхабе. Кроме того, он является кроссплатформенным, а это значит, что для разработки мы можем испольвать как Windows, так и MasOS или Linux.

В то же время TypeScript является типизированным надмножеством JavaScript, а это значит, что любая программа на JS является программой на TypeScript. В TS можно использовать все те конструкции, которые применяются в JS - те же операторы, условные, циклические конструкции. Более того код на TS компилируется в javascript. В конечном счете, TS - это всего лишь инструмент, который призван облегчить разработку приложений.

Типы данных

TypeScript является строго типизированным языком, и каждая переменная и константа в нем имеет определенный тип. При этом в отличие от javascript мы не можем динамически изменить ранее указанный тип переменной.

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

  • boolean: логическое значение true или false

  • number: числовое значение

  • string: строки

  • Array: массивы

  • кортежи

  • Enum: перечисления

  • Any: произвольный тип

  • Symbol

  • null и undefined: соответствуют значениям null и undefined в javascript

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

Большинство из этих типов соотносятся с примитивными типами из JavaScript. Примеры использования:

let num: number = 2;
let phrase: string = 'Я строка';
let phraseArr: string[] = ['Я строка', 'И я строка', 'Пу-пу-пу'];
let isValid: boolean = true;
let someArray: any[] = [24, 'Tom', false];
let person: { name: string; age?: number } = {name: "Tom", age: 23}

Функции

Параметры функции

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

// определение функции
function add(a: number, b: number) {
	let result = a + b;
	console.log(result);
}
// вызов функции
add(20, 30); // 50
add(10, 15); //25

Однако поскольку параметры имеют тип number, то при вызове функции:

add('1', '2');

компилятор TS выдаст ошибку, так как параметры должны иметь тип number, а не тип string.

Результат функции

Функция может возвращать значение определенного типа, который еще называется типом функции. Возвращаемый тип функции ставится после списка параметров через двоеточие:

function add(a: number, b: number): number {
	return a + b;
}
let result = add(1, 2);
console.log(result);

Если функция ничего не возвращает:

function add(a: number, b: number): void {
	console.log(a + b);
}

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

function add(a: number, b: number) {
	return a + b;
}
let result = add(10, 20);

Тип функции

Каждая функция имеет тип, как и обычные переменные. Тип функции фактически представляет комбинацию типов параметров и типа возвращаемого значения. В общем виде определение типа функции выглядит следующим образом:

(параметр1: тип, параметр2: тип,...параметрN: тип) => тип_результата;

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

Например, возьмем следующую функцию:

function hello() {
	console.log('Hello TypeScript');
}

Эта функция не имеет параметров и ничего не возвращает. Если функция ничего не возвращает, то фактически тип ее возвращаемого значения - void. Таким образом, функция hello имеет тип

()=>void;

Объединения union

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

let id : number | string;
id = "1345dgg5";
console.log(id); // 1345dgg5
id = 234;
console.log(id);  // 234

Чтобы определить все типы, которые должно представлять переменная, все эти типы разделяются прямой чертой: number | string. В данном случае переменная id может представлять как тип string, то есть строку, так и число.

Подобным образом можно использовать union для определения параметров функции:

function printId(id: number|string){
    console.log(`Id: ${id}`);
}

Здесь функция printId() в качестве параметра id может принимать как значения типа number, так и значения типа string.

function printId(id: number|string){
    console.log(`Id: ${id}`);
}
 
let id: string|number = "ruy74";
 
printId("1h2e3l4o5");
printId(9876);
printId(id);

Тип union подходит для тех ситуаций, когда логика работы со всеми объединенными типами однообразна, например, как в случае выше, где значение извне встраивается в строку и выводится на консоли. Вне зависимости от типа действия одинаковы.

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

function printSentence(words: string[]|string){
      // если words - строка
      if (typeof words === "string") {
        console.log(words);
      } else {
        // Если words - массив string[]
        console.log(words.join(" "));
      }
}
printSentence(["Язык", "программирования", "TypeScript"]);
printSentence("Язык программирования JavaScript");

В данном случае функция printSentence() может принимать как строку, так и массив строк, чтобы затем вывести строки на консоль. И в зависимости от типа данных действия отличаются. Так, для массива применяется метод words.join(" "), который объединяет все элементы массива в одну строку, разделенные пробелом.

Generics(Дженерики)

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

Ответ: Для этого есть два варианта записи

const str_arr: string[] = ["Hello", "hi", ""]
const str_arr: Array<string> = ["Hello", "hi", ""]

Первый вариант записи является синтаксическим сахаром для второй. Вторая запись в свою очередь является дженериком. Ниже будет секция посвященная дженерикам.

Вопрос: Что же такое дженерики?Что делать, если я передаю аргумент с определенным типом и у меня должен быть выход с точно таким же типом

Ответ: Для таких случаев существуют обобщенные типы, это и есть дженерики

Давайте рассмотрим код, который берет аргумент одного типа и возвращает аргумент такого же типа:

function returnAnyThing<T>(num:T):T {
    return num
}

В данном примере T и является нашим "любым типом". Главная прелесть дженериков в том, что мы отдаем значение с таким же типом, с каким были входные данные (хотя и не обязательно. Суть в том, что мы просто опирируем одинаковыми типами). И нам совсем не важно какого там типа входные данные.

Давайте напишем маленькую функцию. Она берет переменную с любым типом и отдает массив с таким же типом:

function createArray<AnyType>(n: AnyType): AnyType[]{
    return [n];
}

Вот мы и научились пользоваться дженериками. Ничего сложного, достаточно просто запомнить, что они нужны, когда мы производим действия над сущностями с одинаковым типом.

Кастомные типы(type) и интерфейсы(interface)

Кастомные типы

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

type numberString = string | nummber;
const numStr: numberString = "123"

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

Давайте объявим тип с параметрами и попробуем задать его новой переменной:

//Созждаем новый тип объекта
type TPeople = {
    name: string,
    surname: string,
    age: number,
    isMarried: boolean,
    isWorking: boolean,
    hasDog?: boolean
}
// Объявляем переменную с объектом
const people1: TPeople = {
    name: ''
    surname:
    age: 0,
    isMarried: false,
    isWorking: false,
};
const people2: TPeople = {
    name: ''
    surname:
    age: 0,
    isMarried: false,
    isWorking: false,
    hasDog: true
};

Таким образом мы можем строго типизировать объекты

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

Интерфейсы

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

По синтаксису они немного отличаются от типов:

interface название {
    ключ: тип
};

Вопрос: Они отличаются от типов синтаксисом, а смыслом отличаются?

Ответ: Практически нет. Ранее типы использовались для одних кейсов, а интерфейсы — для других кейсов. Ныне это два очень похожих решения по типизизации структур данных, однако различия все же есть.

Возможности, которые есть у интерфейсов, но нет у типов:

  • Декларативное расширение (мерджинг)

  • Расширение интерфейсов

И всего-то?! Давайте рассмотрим сначала две данные фичи, а потом сравним как они реализованы у типов

Декларативное расширение (мерджинг)

Если мы объявим два интерфейса с одинаковыми именами, то TypeScript автоматически "сплюснет" их в один:

interface Person {
    name: string,
    surname: string
}
interface Person{
    age: number
}

//НЕвалидный объект
const Anton: Person = {
    name: "Anton",
    surname: "Zlakov",
}
//Валидный объект
const Mary: Person ={
    name: "Marry",
    surname: "Jane",
    age: 20
}

Расширение интерфейсов

Расширением интерфейса называется процесс, когда один интерфейс поглощает все свойства родителя и добавляет свои:

interface Person {
    name: string,
    surname: string
}

interface Programmer extends Person{
    progLang: string[]
}
//НЕвалидный объект
const Anton: Programmer = {
    name: "Anton",
    surname: "Zlakov",
}
//Валидный объект
const Mary: Programmer ={
    name: "Marry",
    surname: "Jane",
    progLang: ["JS", "TS", "Python"]
}

Вопрос: Это круто, что интерфейсы могут расширяться, но можно же расширить типы, верно?

Ответ: Можно, но никто так не делает, ибо это плохая практика.

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

Вот пример расширения типа:

// Исходный тип
type Person = {
    name: string,
    surname: string
}
// Тип, который расширяет Person
type Programmer = Person & {
    skills: string[]
}
// Валидный объект
const John: Programmer = {
    name: 'John',
    surname: 'Bill',
    skills: ['javascript', 'typescript', 'html', 'css', 'bootstrap', 'vue.js']
};
// Невалидный объект. Ему не хватает skills
const Mary: Programmer = {
    name: 'Mary',
    surname: 'Luino'
}

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

Вообще пересечение у типов работает немного странновато: так как два типа по сути являются подмножествами типа object, то пересечение у них полное (тоже самое что и object & object), а значит все свойства просто соединяются

Проще всего объяснить пересечение и объединение на примере: Представьте, что у нас в двух комнатах есть собаки и кошки. Кошки мяукают, собаки гавкают. Если мы объединим их, то будут кошки или собаки. Так как мы теперь не знаем кто мяукает, а кто гавкает, то мы можем использовать только общие методы и свойства, например: ходить().

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

Кроме того некоторые правила у типов можно обойти с помощью объединения. Разработчик может ошибиться в операции, поставить по привычке | (оператор объединения), и тогда и вовсе второй объект станет валидным. Нам будет достаточно соответствия хотя бы с одним из типов (достаточно свойств из Person или Programmer):

// Исходный тип
type Person = {
    name: string, surname: string
}
// Тип, который дополняет Person
type Programmer = Person | {
    skills: string[]
}
// Валидный объект
const John: Programmer = {
    name: 'John', surname: 'Bill',
    skills: ['javascript', 'typescript', 'html', 'css', 'bootstrap', 'vue.js']
};
// Валидный объект. 
const Mary: Programmer = {
    name: 'Mary',
    surname: 'Luino'
}
//И это тоже валидный объект
const Tom: Programmer = {
    skills: ['javascript', 'typescript', 'html', 'css', 'bootstrap', 'vue.js']
}

При объединении и пересечении запомните простое правило:

  • Объединение - это всегда или

  • Пересечение - это всегда и

Запись с типами кажется сложнее, требуется разглядывать код и разбираться в операторах сложения и умножения для множеств (и/или).

Совет: Старайтесь использовать интерфейсы, если дело касается объектов и сложных структур данных. Используейте типы для создания алиасов (вторых названий) для примитивных типов или для типизации функций.

Задания