TypeScript. Что, зачем и как?

Те из вас, кто занимается разработкой на JavaScript, наверняка так или иначе слышали о TypeScript. Если при этом вы не решались попробовать освоить этот язык из-за неуверенности в его эффективности по сравнению с привычным JS, то сегодня самое время развеять эти сомнения. 

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

Что такое TS, как работает его система типов, и чем он может пригодиться вам в последующих проектах? К концу статьи мы с вами на все эти вопросы ответим. 

Примечание: у меня уже выработалось к этому языку особое отношение, и, честно сказать, я уже предпочитаю вести разработку исключительно на нем, а не на JS.

Что такое TypeScript?

Этот язык можно рассматривать как надмножество JavaScript.

Что это значит?

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

Так что, даже когда мы программируем на TS, конечная программа, выполняющаяся в браузере, будет в JS.

Зачем тогда вообще использовать TypeScript?

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

Как именно TypeScript это делает?

Как и предполагает его имя, он добавляет к JS систему типов. Если в JavaScript тип переменной присваивается динамически, то в TS нам приходится предопределять ее тип сразу в момент объявления.

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

let jsVar = 0;
jsVar = "js";

В случае же с TS можно ограничить это поведение, объявив тип для переменной явно. В итоге если попробовать, к примеру, присвоить строку к переменной типа number, то возникнет ошибка.

let tsVar: number = 0;
tsVar = "ts"; //error
Предупреждение о неверном присваивании типа в VS Code 

По сути, это и отличает TS от JS. В нем используются типы, что позволяет нам избежать в коде глупых ошибок.

Как именно TypeScript улучшает JavaScript

Недостаток типизации нельзя однозначно назвать минусом JavaScript, но это дает программистам слишком большую свободу, что неизбежно ведет к написанию кода с ошибками.

let aNumber = 123;aNumber = {
    name: "John",
    age: 23
}

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

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

let aNumber: number = 123;

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

function isEligible(personObj) {
    return personObj.age > 34;
}
let john = {
    name: "John",
    age: 23
};
isEligible(john);

В этом случае функция isEligible ожидает объект с полем age. Но в JS нет способа гарантировать, что передаваемый аргумент будет именно объектом, или что он будет содержать поле age.

Опять же в TS есть для этого решение.

interface Person {
    name: string;
    age: number;
}
function isEligible(personObj: Person) {
    return personObj.age;
}
let john = {
    name: "John",
    age: 23
};
isEligible(john);

Пока что этот код может быть для вас не понятен. Но обратите внимание  —  он гарантирует, что передаваемая переменная имеет тип Person, который определяется в начале.

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

Если вам не хватало вариантов автоподстановок для JS-кода в IDE, то тем более стоит познакомиться с TS. Наличие типов дает этому языку возможность предлагать в IDE более точные подстановки.

Использование типов в TypeScript

Базовые типы

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

Полный список можно найти в документации TypeScript (англ.).

Вот несколько примеров:

const num: number = 0;
const firstName: string = 'Juan';
const isValid: boolean = true;
const obj: object = {
    id: 1,
    name: 'Juan',
};
// Два способа определения массивов
const names: string[] = ['Juan', 'Sean', 'Jane'];
const dogs: Array<string> = ['Rex', 'Woof', 'Puppy'];
// Переменные типа any могут иметь любой тип
let newVar: any = 'Hello World';
newVar = 89;
newVar = false;
// Тип можно определять как объединение нескольких типов
let numOrBoolean: number | boolean = 12;
numOrBoolean = true;
numOrBoolean = "hey"; // error

Обратите внимание, как тип any приводит к тому, что TS ведет себя аналогично JS. Поскольку целью использования TS является именно улучшение структуры кода, то лучше стараться избегать any.

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

Объявление пользовательских типов

Помните, как я использовал тип Person в примере ранее? Но Person не относится к базовым типам TS. Я создал этот тип по своей необходимости, чтобы задействовать его как тип параметра, принимаемого заданной функцией. 

Для определения основной структуры нового вводимого в приложение типа используются интерфейсы.

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

Теперь если создать новый объект типа Person, он должен содержать поля name и age. Если одного из них не будет, возникнет ошибка.

Уведомление об отсутствии свойств в пользовательском типе. VS Code.

В интерфейсе также можно определять необязательные поля.

interface Address {
    houseNumber: number,
    street: string,
    city: string,
    country?: string
}
const address1: Address = {
    houseNumber: 134,
    street: "Down road",
    city: "Berlin",
    country: "Germany"
}
const address2: Address = {
    houseNumber: 2254,
    street: "Up road",
    city: "London",
}

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

interface Person{
    name: string;
    age: number;
    address: Address;
}

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

В TS можно реализовать наследование свойств от другого типа, расширив его интерфейс. 

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

Это можно быстро реализовать через расширение интерфейса Person.

interface Person {
    name: string;
    age: number;
}
interface Employee extends Person {
    jobName: string;
    salary: number;
}
const employeeJohn: Employee = {
    name: "John",
    age: 34,
    jobName: "Javascript developer",
    salary: 54000
}

Типы параметров функций и возвращаемые типы

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

interface Car {
    id: number;
    color: string;
    sold: boolean;
}
function getSoldCarCount(cars: Array<Car>) : number {
    return cars.reduce<number>((acc, car) => acc + car.sold ? 1 : 0, 0);
}
const car1: Car = {
    id: 23,
    color: "red",
    sold: false
}
const car2:Car = {
    id: 78,
    color: "black",
    sold: true
}
const car3: Car = {
    id: 12,
    color: "yellow",
    sold: true
}
const cars: Array<Car> = [car1, car2, car3]
let soldCarCount = getSoldCarCount(cars);

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

Вы также гарантируете, что поле sold в любом передаваемом объекте не будет undefined или null. Кроме того, это исключает ряд сценариев, в которых при выполнении могла возникнуть ошибка. Если бы мы в этом случае использовали JS, то для предотвращения подобных ошибок пришлось бы написать больше кода. 

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

function buyCar(car : Car): Car | boolean {
    if (car.sold === true){
        return false;
    }
    return car;
}

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

enum Manufacturer {
    Fiat,
    Porsche,
    Audi,
    BMW
}
interface ImportedCar extends Car {
    manufacturer: Manufacturer
}
const newImportedCar: ImportedCar = {
    id: 456,
    color: "black",
    sold: false,
    manufacturer: Manufacturer.Fiat
}
buyCar(newImportedCar);

Обобщения

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

function getInfo<T>(input: T): T {
    return input;
}const stringInfo = getInfo<string>("Hello World");
const numberInfo = getInfo<number>(3321);
const carInfo = getInfo<Car>(car1);

А если использовать вместо обобщений тип ‘any’?

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

function getInfo(input : any) {
    return input;
}const stringInfo: string = getInfo("Hello World");
const numberInfo: number = getInfo(3321);
const carInfo: Car = getInfo(car1);

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

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

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

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

function accept(input: string): string | boolean | number {
    if (input.length > 13){
        return false;
    } else if (input.split("/").length < 3){
        return input.split("/").join(".");
    } else {
        return input.split("/").length;
    }
}
function reject(input: string): string | boolean | number {
    if (input.length > 13){
        return true;
    } else if (input.split("/").length < 3){
        return input.split("/").join(".");
    } else {
        return input.split("/").length;
    }
}

Вместо того, чтобы каждый раз переписывать это объединение, как показано выше, можно определить для него псевдоним с помощью ключевого слова type.

type resultType = string | boolean | number;
function accept(input : string) : resultType {
    ...
}
function reject(input : string) : resultType {
    ...
}

Теперь не придется прописывать длинное объединение типов. К тому же, если в будущем понадобится внести изменения в возвращаемый тип функции, то теперь потребуется изменить только одну строку кода.

Преобразование типов

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

Возьмем ранее определенные типы Car и ImportedCar. Сначала создадим объект типа ImportedCar и на его примере посмотрим, как это преобразование работает.

const newImportedCar: ImportedCar = {
    id: 456,
    color: "black",
    sold: false,
    manufacturer: "fiat"
}
const convertedCar = <Car>newImportedCar; //no error
//Альтернативный синтаксис для преобразования
const anotherConvertedCar = newImportedCar as Car;

Этот код компилируется без ошибок. Данное преобразование работает, так как ImportedCar уже содержит все поля, определенные в Car.

Если попробовать обратиться к полю manufacturer, определенному в объекте до преобразования, то возникнет ошибка, поскольку преобразованный объект имеет тип Car.

convertedCar.manufacturer;  //error

Этот процесс работает и в обратную сторону. Можно преобразовать объект Car в объект ImportedCar.

const newCar: Car = {
    id : 234,
    color: "green",
    sold: true,
}
const convertedImportedCar = <ImportedCar> newCar;

В этом случае, если попробовать обратиться к новому полю, которого ранее у объекта Car не было, то есть manufacturer, то в ответ мы получим undefined.

convertedImportedCar.manufacturer;   //undefined

Заключение

Надеюсь, эта статья рассеяла ваши сомнения по поводу использования TS для фронтенд разработки. Поскольку большинство возможностей TS повторяют JS, вы сможете освоить этот язык очень быстро, получив ощутимую отдачу уже в ближайших проектах.

Уверен, что вскоре вы наверняка станете JS-разработчиком, который просто не мыслит жизни без TypeScript, каким и стал я.

Благодарю за чтение!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Juan Cruz Martinez: The What, Why, and How of TypeScript for JavaScript Developers

Предыдущая статья8 частых ошибок в Android-разработке
Следующая статья7 библиотек Python для вашего первого проекта по науке о данных