TypeScript
TypeScript (TS, TScript или «тайпскрипт») — это язык программирования для веб-разработки, основанный на JavaScript. Делает код понятнее и надежнее, добавляет статическую типизацию (переменные привязаны к конкретным типам данных), а также может быть скомпилирован в JavaScript. TypeScript используют фронтенд- и бэкенд-разработчики.
Освойте профессию «Frontend-разработчик»

Отличия TypeScript от JavaScript
Строгая типизация
Данные в JS хранятся в переменных и имеют какой-либо тип: строка, число, логическое значение. В JavaScript нестрогая, динамическая типизация. Это значит, что одной переменной можно присвоить сначала числовое значение, а потом, например, текстовое.
TypeScript добавляет в язык строгую типизацию. Каждой переменной при создании присваивается определенный тип (type) — стандартный или созданный самим разработчиком. Создать тип можно в пределах возможностей языка: например, число от 1 до 31 для записи дня в месяц или массив из двух элементов для записи координат.
Что такое «переменной присваивается тип»? Это значит, что на протяжении всего скрипта она сможет принимать только значения в рамках этого типа. Переменная, заданная как число, будет только числом. Если попытаться присвоить ей строковое значение, программа выдаст ошибку.
Помимо примитивных типов, уже существующих в JavaScript, — boolean, bigint, null, number, string, symbol, object и undefined, — TypeScript имеет уже знакомые нам типы any (разрешает использовать любое значение) и void (тип для функций, которые ничего не возвращают, у них нет return).
Профессия / 9 месяцев
Frontend-разработчик
Создавайте интерфейсы сервисов, которыми пользуются все

Объектно-ориентированный подход
ООП, или объектно-ориентированный подход, рассматривает информацию, с которой работает, как классы (classes) и объекты (objects). Объект — это сложный экземпляр данных, а класс — «чертеж» или «каркас», на основании которого создаются объекты. Объект имеет класс, так же как переменная имеет тип.
В JavaScript есть возможности, которые позволяют реализовать ООП, но только частично. Язык поддерживает классы и объекты, и этого достаточно для базовых действий. Но, например, он не работает с модификаторами доступа — так называют возможность сделать какую-то часть класса «открытой» или «закрытой» для доступа со стороны.
TypeScript поддерживает три модификатора:
- Public — элементы с этим модификатором доступны отовсюду без каких-либо ограничений. Этот модификатор установлен по умолчанию.
- Private — элементы с этим модификатором доступны только из того класса, в котором они определены.
- Protected — элементы с этим модификатором доступны из класса, в котором они определены, и в подклассах/производных классах.
TypeScript добавляет в JS множество возможностей ООП: и модификаторы доступа, и абстрактные классы, и другие особенности подхода.
Высокая скорость работы над проектом
TypeScript помогает сократить время на выявление и устранение багов, которые иногда сложно найти в динамической среде JavaScript. С помощью TypeScript можно написать более понятный и читаемый код, который максимально описывает предметную область. Таким образом архитектура становится более выраженной.

Станьте Frontend-разработчиком
и создавайте интерфейсы сервисов, которыми пользуются все
Как работает TypeScript
Код, написанный на TypeScript, не выполнится напрямую в браузере. Поэтому TS — не самостоятельный язык, а именно языковая надстройка над JS. Для его работы нужен дополнительный этап — транспиляция, когда программное обеспечение преобразует написанный на TypeScript код в «чистый» JavaScript.
JS не требует установки в систему: его по умолчанию поддерживает любой браузер. А вот TypeScript понадобится установить, потому что для транспиляции необходим модуль tsc. Установите Node.JS и загрузите пакет для программирования на TypeScript через менеджер NPM:
npm install -g typescript
После написания программы на TypeScript вы получите файл с расширением .ts. Его нужно будет скомпилировать и преобразовать в JS с помощью модуля tsc. Это делается через консоль:
Пример кода
const getUsers = (): ThunkAction, <>, AnyAction> =>
async (dispatch: ThunkDispatch, <>, AnyAction>) => //…
try const users: User[] = await APIService.get(‘/users’)
dispatch(successGetUsers(users))
> catch(err) dispatch(failedGetUsers(err))
>
>
Попробовать написать и запустить код на TypeScript можно прямо из браузера на этой странице, без локальных настроек.
Плюсы TypeScript
- Ошибки, допущенные в процессе модификации кода, видны сразу, а не во время выполнения.
- В TypeScript есть интерфейсы и классы.
- Создатель — Андерс Хейлсберг, который также разработал C#, Turbo Pascal и Delphi.
- Имеет открытый исходный код, доступный на GitHub.
- TypeScript позволяет быстрее и проще писать комплексные решения, которые в дальнейшем будет легче развивать и тестировать.
- Язык прост в изучении. Есть множество мануалов.
- TypeScript имеет высокую скорость компиляции и распространяется по лицензии Apache.
Минусы TypeScript
- Невозможно типизировать ошибки, появившиеся в режиме реального времени.
- Не все браузеры поддерживают вкладку TypeScript в консоли, что приводит к большим временным затратам.
- TypeScript имеет множество нетривиальных классов.
- Несмотря на статическую типизацию, можно допустить прохождение любого типа данных, прописав слово any.
Frontend-разработчик
Научитесь создавать удобные и эффектные сайты, сервисы и приложения, которые нужны всем. Сегодня профессия на пике актуальности: в России 9000+ вакансий, где требуется знание JavaScript.

Статьи по теме:
- Angular
- Go (Golang)
- Как новичку начать пользоваться GraphQL и зачем это нужно
TypeScript: худший лучший язык программирования
На конференции FrontedConf 2021 Андрей Старовойт показал плюсы и минусы TypeScript. Если вы сомневаетесь, стоит ли его использовать — эта статья для вас, чтобы вы смогли для себя всё решить. Если вы уже любите и используете TypeScript, то надеюсь, вам тоже будет интересно.
Все преимущества и недостатки языка описаны, конечно, через призму опыта Андрея. Несмотря на то, что последние 7 лет он работает в компании JetBrains над продуктом WebStorm на Java Kotlin, пишет он и на TypeScript. Попутно много смотрит на код других людей, пытаясь понять, что с ним можно сделать внутри WebStorm и почему типы выбились неправильно. А также — какие инспекции можно применить так, чтобы люди стали счастливы, а их код — лучше.

Самый неочевидный аспект TypeScript — в нем нет синтаксического сахара (ну почти). Вместо этого язык реализует типовую систему для JavaScript. Часто говорят, что это минус TypeScript: «Мы могли бы сделать JavaScript гораздо более эффективным в плане написания кода, добавив каких-нибудь магических конструкций, которые компилировались бы в эффективный JavaScript!». Но команда TypeScript так не делает.
Точнее, поначалу они попробовали, добавив namespace и enum. Но сейчас это считается не очень удачными экспериментами, и TypeScript больше не добавляет новых фич, связанных с синтаксическим сахаром. Во многом это обусловлено тем, что JavaScript активно развивается, а TypeScript — это надстройка над JavaScript. То есть мы и так автоматически получаем все новые синтаксические конструкции из спецификации языка.
Теперь давайте посмотрим, из чего состоит TypeScript, и какие могут быть сложности с каждой из его особенностей.
Типы TypeScript
Достаточно знать несколько типов?
Типы — это основная концепция, связанная с TypeScript и то, ради чего этот язык задумывался. Если открыть цели команды TypeScript, то там явно написано: они разрабатывают статическую типовую систему для JavaScript.
Люди очень часто говорят, что TypeScript — это небольшая надстройка, superset над JavaScript, который добавляет типы. И что достаточно изучить несколько типов, чтобы начать писать на TypeScript и автоматически получать хороший код.
Действительно, можно просто писать типы в коде, объясняя компилятору, что в данном месте мы ожидаем переменную определенного типа. А компилятор скажет, можно так делать или нет:

Тем не менее не получится переложить всю работу на компилятор. Давайте посмотрим, какие типы надо изучить, чтобы понимать TypeScript.
- Начнем с базовых типов, которые есть и в JavaScript: это boolean, number, string, symbol, bigint, undefined и object. Вместо типа function в TypeScript есть Function и отдельный синтаксис, подобный arrow function, но для определения типов. А тип object будет означать, что переменной можно присвоить любые объектные литералы в TypeScript.
- Дальше есть примитивные, но уже специфичные для TypeScript типы: null, unknown, any, void, unique symbol, never, this.
- Что еще? Named и object (не путать с object). Первый используется, когда мы пишем какое-то название интерфейса и после двух точек говорим, что у переменной тип Foo. У этого типа есть много разных названий, например, reference type, но мы остановимся на named. Тип object позволяет описать внутреннюю структуру объекта в виде специального синтаксиса. К сожалению, в терминологии TypeScript он называется точно так же, как и примитивный object.
- Далее идут стандартные для многих объектно-ориентированных языков типы: array, tuple, generic.
Казалось бы, на этом можно остановиться, потому что если говорить про типовую систему той же Java, то больше ничего не нужно. Но TypeScript не останавливается: он предлагает union и intersection. В связке с этими типами часто работают и особые литеральные типы: string, number, boolean, template string. Они используются, когда функция принимает не просто строку, а конкретное литеральное значение, как “foo” или “bar”, и ничего другого. Это существенно повышает описательную способность кода.
Вроде бы уже достаточно, но нет! В TypeScript есть еще: typeof, keyof, indexed, conditional, mapped, import, await, const, predicate. И это лишь базовые типы, на их основе строятся многие другие. Например, композитный Record , который встроен в стандартную библиотеку. Или внутренние типы Uppercase и Lowercase , которые никак не определяются: это intrinsic типы.
Вроде бы уже достаточно сложно, чтобы не изучать TypeScript? Но трудности еще не закончились!
Выразительность типовой системы TypeScript
В 2017 году на GitHub появилась запись, что типовая система TypeScript является Turing Complete. То есть на типах TypeScript можно написать машину Тьюринга:

Задумайтесь — выразительная способность типовой системы TypeScript настолько высокая, что она Turing Complete и позволяет писать любые программы просто на типах! Но что с этим может быть не так? Давайте рассмотрим очень простую функцию changeCase, которая в зависимости от флага low делает строчке либо LowerCase(), либо UpperCase():
function changeCase(value, low)
Это довольно очевидный способ написать функцию как в JavaScript, так и в TypeScript. Но можно сделать и так:
declare function changeCase(value: T, low: Q): Q extends true ? Lowercase : Q extends false ? Uppercase : string changeCase("FOO", true); //type "foo" changeCase("foo", false); //type "FOO"
Кажется, этот код невозможно прочесть, но идея в том, что когда мы передаем в нашу функцию значение true и какой-то строковый литерал, то на уровне типов мы получаем правильное итоговое значение. Задумайтесь! Мы не выполняем нашу функцию, но знаем, что она вернет для конкретной комбинации параметров (для флага true и для флага false).
Выразительность TypeScript позволяет делать просто умопомрачительные вещи. Вы можете не просто сказать, что функция вернет какое-то значение, а описать, что конкретно она будет возвращать даже для частных случаев.
Но есть нюанс. При попытке точно описать всё, что делает функция — мы легко можем попасть в ситуацию, когда в тайпингах какой-нибудь известной библиотеки (например, Styled Components) совершенно невозможно понять, что там происходит. Вот пример:

Здесь можно увидеть интерфейс ThemedStyledFunction, а в нем — набор generic параметров, которые выполняют совершенно непонятную функцию. Кроме того, интерфейс расширяет какой-то ThemedStyledFunctionBase.
Размотать эту цепочку и понять, что делает функция, практически невозможно без редактора, который хорошо поддерживает TypeScript. Кроме того, когда у нас не «срослись» типы, ситуация еще больше усугубляется. Для всего этого надо уметь ходить в декларации, по десяткам библиотек, которые друг друга наследуют и расширяют. В итоге мы уже не можем писать, как в старые добрые времена, на JS в каком-нибудь Sublime Text без языковой поддержки.
Конечно, мы сейчас говорим не про IDE, а про любой «умный» редактор, где есть языковой сервис. Например, это может быть Vim с поддержкой TypeScript Language Service.
Многие вещи всё ещё трудно выразить
Самое смешное, что несмотря на Turing-полноту, выразительность TypeScript все еще недостаточная, чтобы описать некоторые функции, которые есть в стандартной библиотеке JavaScript. Например, декларация Object.assign() выглядит в TypeScript 4.5 следующим образом:
assign(target: T, source: U): T & U; assign(target: T, source1: U, source2: V): T & U & V; assign(target: T, source1: U, source2: V, source3: W): T & U & V & W; assign(target: object, . sources: any[]): any;
Для двух, трех и даже четырех параметров мы еще возвращаем intersection, а для пяти уже сдаемся. В некоторых библиотеках можно увидеть до 90 таких сигнатур с разным количеством параметров. Здесь, как нельзя кстати подходит этот твит:

С типами мы пока закончили, переходим к другим сложностям.
Структурная типизация
Что такое структурная типизация? Это подход, при котором мы смотрим не на то, как называется тип или где он определяется, а на то, что он описывает внутри. Например, есть два интерфейса, которые определяют поле foo. Для TypeScript эти два интерфейса одинаковые, он не различает их в момент использования. Вы можете взять переменную одного интерфейса, присвоить в переменную другого, и всё будет работать:
interface Foo1 < foo: string >interface Foo2 < foo: string >let foo1: Foo1 = < foo: "text1" >; //ok let foo2: Foo2 = < foo: "text2" >; //ok foo1 = foo2; //ok
В TypeScript используется этот подход потому, что очень часто в JavaScript мы работаем с объектными литералами, которые не привязаны ни к какому типу. Довольно логично, что в таком случае, когда мы определяем просто объект с полем foo, то он может быть присвоен как первому интерфейсу, так и второму.
Перейдем к проблемам:
interface Foo < foo: string >interface Bar < bar: string >declare let foo: Foo; declare let bar: Bar; foo = bar;
Если присвоить две переменных из разных интерфейсов, то мы получим сообщение об ошибке:

Из-за структурной типизации мы уже не можем просто получить сообщение, что интерфейс Foo не совместим с интерфейсом Bar (или наоборот). Мы должны сказать, что одну переменную нельзя присвоить в другую, потому что в одном из интерфейсов не хватает какого-то поля, или в другом интерфейсе их слишком много. То есть нам нужно понимать внутреннюю структуру объекта и информировать о том, в каком именно месте типы не сошлись. Это легко, когда у нас вложенность первого уровня. Но если у нас вложенность на десятки, и тип не сошелся где-то очень глубоко, то выглядеть это может так:

Это реальное сообщение об ошибке, когда пользователь неправильно добавляет атрибут, при использовании Styled Components в TypeScript. Такое сообщение не только невозможно прочитать, но еще и не дает никакой информации о том, в чем именно проблема.
Структурная типизация для классов
Небольшой бонус: в TypeScript структурная типизация используется еще и для классов, и это просто магическая вещь. Например, у нас есть класс с полем foo:
class ClassFoo < foo?: number >function test(p: ClassFoo) < if (!(p instanceof ClassFoo)) < //p is never here console.log("hello never"); >>
Как работает компилятор для этого кода? Внутри функции TypeScript знает, что переданный параметр p имеет тип ClassFoo. С другой стороны, внутри instanceof он не должен быть ClassFoo. То есть мы никогда не сможем попасть внутрь этого блока кода. Исходя из этого TypeScript считает, что тип переменной p внутри блока — это never. Но невозможное возможно!
class ClassFoo < foo?: number >function test(p: ClassFoo) < if (!(p instanceof ClassFoo)) < //p is never here console.log("hello never"); >> test(<>); //prints “hello never”
За счет структурной типизации пустой объект все еще будет совместим на уровне типов с классом ClassFoo. Мы сможем передать его в эту функцию, где выводится сообщение «hello never» — чего, если верить типовой системе TypeScript никогда не должно случиться. Вот такая магия.
Анализ кода и Type Guard
Вы не обязаны проставлять типы повсеместно. Иногда TypeScript понимает сам, что в данном контексте после применения нескольких if-блоков у переменной будет правильный тип, и можно обращаться к свойствам этой переменной напрямую. Таких механизмов анализа в TypeScript довольно много, и это то, за что можно любить TypeScript. Подобные механизмы анализа есть и в Kotlin, а Java так не умеет.
Простой пример — есть код, мы его скомпилировали и получили ошибку:

Получили ошибку потому, что typeof null — это object. И компилятор TypeScript это знает.
Не очень опытные JS-разработчики могут не знать этого факта и допускать такие ошибки. А TypeScript знает и помогает написать более безопасный код. Посмотрим на другим примере, какие проблемы могут быть с таким анализом кода в TypeScript:

Какой тип у result: string или “bar” | “foo”? Видимо, string, раз в итоге ошибка компиляции. Но самое смешное — это то, как можно исправить эту проблему:
function rand(): "bar" | "foo"
Просто написали const вместо let — и все скомпилировалось! Теперь по мнению компилятора, очевидно, что тип у result будет “bar” | “foo”.
Спецификация?
Вопреки ожиданиям, спецификация TypeScript не поможет разобраться в сложных алгоритмах вывода типов (с использованием Control Flow / Data Flow) — спецификации просто не существует уже много лет. До версии 1.8 она еще была, но после этой версии разработчики выпускают только handbook. Потому что считают, что спецификация никому не нужна, а работы для ее поддержания в актуальном состоянии требуется очень много. Даже сам файл спецификации из репозитория перенесли в архив, чтобы люди не пытались его редактировать.
Теперь давайте пройдемся по этим же пунктам снова и попробуем понять, так ли всё плохо и можно ли эти проблемы решить.
Реальность: так ли всё плохо на самом деле?
Начнем с того, что наличие сложных типов в языке не обязывает вас использовать их. Но есть очень важный нюанс — вы должны знать типы, потому что иначе вы не поймете код в тех же библиотеках. Для примера можно посмотреть, насколько часто используются сложные типы в исходном коде TypeScript и тайпингах React (react.d.ts):
Типы
TypeScript
react.d.ts
Явно определяется тип, включая интерфейсы и все места, где после двоеточия стоит тип (включая вложенные)
Используется тип Conditional
То есть в репозитории TypeScript, на 67 000 определений с типами их 23, а здесь из 430 мест — целых 37!
Используется тип Mapped.
Хорошо иллюстрирует эту ситуацию твит от одного из создателей TypeScript:

Райан говорит, что проблема не с типовой системой TypeScript, а с экосистемой JavaScript: она настолько сложная, что ей требуются эти типы. А так — да, вас никто не заставляет их использовать при написании кода, при проектировании ваших API.
Тем не менее, типизация, если использовать ее аккуратно, делает любую функцию гораздо лучше. Например, наш замечательный пример с changeCase можно переписать следующим образом:
function changeCase (value: string, low: boolean): string
Она уже не будет так эффектно выводить типы, но это будет читаемый код.
С другой стороны, высокая выразительность типовой системы TypeScript подводит нас к очень важной идее: типовая система — это язык программирования. И к его использованию применяются соответствующие требования, как к обычному коду, который мы пишем. То есть мы должны делать его понятным, не должны делать over-engineering и т.д.
Тогда другим вариантом типизации функции changeCase будет явное прописывание нескольких вариантов сигнатур. Это уже чуть лучше, хотя всё еще не идеально. Для случаев true и false просто определяются отдельные сигнатуры, плюс мы пишем общую сигнатуру, которая получится в итоге, если мы не знаем тип:
function changeCase (value: TString, low: true): Uppercase function changeCase (value: TString, low: false): Lowercase function changeCase (value: string, low: boolean): string
Понятно, что это по-прежнему не очень читаемо, но уже гораздо лучше, чем двойной conditional тип, который был до этого.
TypeScript — это во многом про то, как надо и как не надо писать код, в также про соглашения и стиль написания кода. Конечно, многие вещи характерные для TypeScript можно реализовать на уровне каких-то конвенций: мы просто договариваемся внутри команды, что делаем явное приведение типов, не пишем какие-то конструкции и не присваиваем в переменную 10 разных типов, только чтобы ее переиспользовать, и т.д.
TypeScript позволяет это сделать более строго, и вам уже не нужно задумываться про такие мелочи. Вы будете знать, что на уровне TypeScript у вас уже есть некоторая строгость, которая определяет то, как ваша команда будет писать код.
Например, для JavaScript такой код будет абсолютно валидным:
console.log(Math.sin("3.1415"))
TypeScript скажет, что это неправильно:

Разработчики, которые только начинают писать код, могут быть не очень опытными и не совсем понимать, как писать правильно, а как писать нельзя. И TypeScript им сможет это подсказать.
Вернемся к случаю, когда тип переменной был не очень понятен. Использование const вместо let на самом деле — всего лишь трюк, о котором нужно знать. А правильное исправление — это добавлении типа:
function rand(): "bar" | "foo" < let result: : "bar" | “foo" = Math.random() < 0.5 ? "foo" : “bar"; return result;
Для TypeScript явное всегда лучше, чем неявное: когда мы говорим, что здесь используется конкретный тип, то мы сразу же убираем всю сложность, которую привносит неспецифицированный анализ типов в TypeScript.
На самом деле это касается и типов возвращаемых значений функций. Чтобы избавиться от всей магии, когда TypeScript начинает применять свои внутренние правила про то, как вывести и расширить тип литерала и т.д., можно просто указать явный тип. Читаемость кода значительно повысится за счет того, что люди будут знать, что эта функция возвращает.
Что касается редактора с языковой поддержкой, то он помогает решать огромное количество проблем, а сам процесс написания кода становится очень удобным. Потому что TypeScript даёт обратную связь в момент написания кода, а не в момент тестирования:

Конечно, сила TypeScript не только в этом, как мы уже увидели. Подсказки от редактора IDE могут значительно повысить продуктивность при написании кода. Да, мы чем-то пожертвовали: мы уже не можем писать код в блокноте. Но при этом мы выигрываем огромное количество времени просто за счет того, что редактор подсвечивает типы, говорит, что передавать в данную функцию и пр.
На заметку
О чем стоит помнить? Во-первых, что TypeScript — это индустриальный стандарт типизации. Текущее состояние JavaScript мира таково, что про типизацию — это TypeScript и ничто другое. Сейчас нет другого решения, которое бы позволило бы эффективно внедрить типизацию в проект. Можно, конечно, использовать какие-то контракты, конвенции или JSDoc для описания типов. Но все это будет гораздо хуже для читаемости кода по сравнению с типовыми аннотациями TypeScript. Они позволят не метаться вам глазами вверх-вниз, вы будете просто читать сигнатуру и тут же все понимать.
Второй момент — поддержка JavaScript в редакторах и IDE, как правило, базируется на TypeScript. Этот пункт очень нетривиален, но всегда забавно, когда говорят, что Visual Studio Code нормально писать на JavaScript и без TypeScript, что там и так всё работает. Потому что поддержка JavaScript во всех современных редакторах IDE строится на TypeScript! Поддержка JavaScript в VS Code реализована с помощью TypeScript Language Service. Поддержка JavaScript в WebStorm по большей части полагается на типовую систему TypeScript, и использует ее стандартную библиотеку.
Это наша реальность — вся поддержка JavaScript в редакторах строится поверх TypeScript. То есть нам в любом случае нужно изучать TypeScript. Потому что когда редактор говорит, что не сошлись типы в JavaScript, нам придется читать декларации из TypeScript.
Третий нюанс — Angular использует TypeScript как язык по умолчанию. Раньше у них на сайте можно было выбрать: «Покажи мне, как писать код на Angular в Dart (или в JS)». Но де-факто на Angular, кроме как c использованием TypeScript, никто не пишет. Если вы хоть раз пробовали писать на Angular без TypeScript — вы знаете, что это боль и страдание.
И наконец, TypeScript не заменяет другие инструменты повышения качества кода. TypeScript — это всего лишь один из инструментов, который позволяет вести какие-то конвенции в проекте и сделать так, чтобы были типы. Но вам все равно нужно писать тесты, делать код-ревью и уметь правильно проектировать архитектуру.
Выводы
- TypeScript имеет много проблем, но, по мнению Андрея, плюсы перевешивают, причем значительно.
- Вы не обязаны использовать TypeScript для каждого проекта. Если вы уже писали на TypeScript, то вы будете точно также хорошо писать код на JavaScript — у вас уже есть шаблон, как делать правильно, а как — нет. Это понимание приходит с опытом, после чего можно довольно гибко выбирать, где использовать TypeScript, а где нет.
- Но если вы создаете внешнюю библиотеку, то у вас нет выбора: люди будут ее использовать в том числе с TypeScript. Для этой библиотеки должны быть типовые декларации. Единственный нормальный способ их получить — это написать библиотеку на TypeScript. Точно такая же ситуация, если вы делаете какой-то npm-пакет, которым будут пользоваться другие люди.
Профессиональная конференция фронтенд-разработчиков FrontendConf 2022 пройдет 7-8 ноябре в Сколково, Москва. Уже можно забронировать билеты и купить записи выступлений с прошедшей конференции FrontendConf 2021.
До 22 мая все еще открыт CFP, и, если вы хотите выступить, то подумайте об этом — Программный комитет ждет ваши заявки. Чтобы помочь вам развеять сомнения или уточнить тему для выступления — 28 апреля в 19:00 Программный комитет проводит онлайн-встречу. Регистрируйтесь и приходите, чтобы всё обсудить и понять, как лучше «упаковать» вашу тему!

- frontend
- typescript
- javascript
- разработка сайтов
- структурная типизация
- типы TypeScript
- программирование
- веб-разработа
- turing-complete
- turing-completeness
- Блог компании Конференции Олега Бунина (Онтико)
- Веб-разработка
- JavaScript
- Программирование
- TypeScript
TypeScript: понятное руководство для новичков

TypeScript — это язык программирования для разработки современных веб-приложений, расширяющий возможности уже традиционного JavaScript. В своей статье редакция highload.today разобралась, что такое TypeScript, какими он обладает преимуществами и особенностями.
Курс UI/UX для геймдеву.
Під час навчання ви розробите проекти для портфоліо, що складається з 5 ключових аспектів UX/UI-дизайну, та отримаєш необхідні навички для професійного росту.

1. Что такое TypeScript?
TypeScript — это язык со статической типизацией на основе JavaScript. Он создан в 2012 году в компании Microsoft. Разработкой TypeScript занимался Андрес Гейлсберг (тот самый, который был ведущим архитектором C# и Delphi, а также создал Turbo Pascal).

TypeScript — это JavaScript с дополнительными возможностями
Строгая типизация
Для переменных и других структур данных можно объявить определенный тип, например, строковый или булевский, и TypeScript будет проверять, допустимые ли значения им присваиваются. Это невозможно сделать в JavaScript, в котором типизация нестрогая.
В TypeScript можно явно указать тип переменной. Когда тип переменной определен, ей невозможно присвоить значение, которое относится к другому типу.
Курс Project Manager.
Впроваджуйте покроковий алгоритм управління проєктами вже зараз. У цьому вам допоможе Павло Харіков — Head of IoT у Veon Group (Kyivstar).

Число остается числом
Расширенные возможности ООП
В TypeScript реализованы дополнительные возможности ООП, такие как интерфейсы, поля объекта и модификаторы доступа.
Интерфейс определяется с помощью ключевого слова interface .
// Определить интерфейс домашнего животного interface IPet < name: string, species: string, breed: string, speak: () =>string > // Создать пса var dog:IPet = < name: "Ritchie Blaсkdog", species: "dog", breed: "hellhound", speak: ():string => >
Поля и модификаторы доступа:
class User < // Поля будут приватными private name: string; private password: string; constructor(name: string, password: string)< this.name = name; this.password = password; >// Доступ для чтения открыт public get Name(): string < return this.name; >public get Password(): string < return this.password; >>
2. Отличия TypeScript от JavaScript
JavaScript создан для написания небольших сценариев веб-страниц. Чем крупнее приложение, тем более громоздким становится код на JS.
Поэтому был создан TypeScript, который упрощает разработку широкомасштабных приложений.
Из-за нестрогой типизации в JavaScript многие ошибки видны только во время выполнения. Это неудобно и, в конце концов, неприятно. Приходится тратить много времени на поиск таких ошибок.
TypeScript устраняет этот недостаток: написанный на нем сценарий не сможет случайно вызвать переменную как функцию или запросить значение поля, если оно не определено в объекте. Вы увидите превентивное сообщение об ошибке уже во время набора кода.

Ошибки видны во время разработки
В TypeScript реализована поддержка интерфейсов и модификаторов доступа, обобщений, модулей, пространств имен и окружений ( ambients ). В JavaScript этого нет.
Код JavaScript — интерпретируемый язык. Он воспринимается браузерами напрямую. TypeScript компилируется в JavaScript.
Курс Frontend.
Онлайн курс з працевлаштуванням, ставай суперніндзя, який може з нуля створити сайт.
3. Установка TypeScript
Осваивать язык программирования лучше всего на практике. Для тестирования небольших фрагментов кода существует TypeScript Playground. Здесь вы можете набирать код, просматривать ошибки, журнал, код JavaScript, в который скомпилирован ваш код на TypeScript, настраивать среду по своему усмотрению, экспортировать код и скопировать ссылку, чтобы поделиться им.
Бонус — код копируется с форматированием.

Удобная «игровая площадка» для знакомства с TypeScript и тестирования сниппетов
Для разработки проектов вам понадобится установить компилятор TypeScript. Его можно установить с помощью менеджера зависимостей для Node.js: npm, yarn или pnpm.
Предположим, что Node.js и менеджер зависимостей npm установлены. Создадим папку проекта и в командной строке запустим такую команду:
npm install typescript --save-dev
С помощью этой команды установки создается локальная копия TypeScript в папке проекта. Для рабочих проектов рекомендуется использовать именно такую установку.
Можно установить TypeScript и глобально:
npm install -g typescript
В этом случае команда tsc будет доступна в любом расположении. Глобальную установку можно использовать для одноразовых испытаний.
4. Как работает TypeScript?
Теперь проверим установку. В этой же папке создадим файл dog.ts (файлы TypeScript имеют расширение ts ).
Добавим в него код из примера про пса, который приведен выше, и сохраним файл. В командной строке введем:
npx tsc dog.ts
После выполнения команды получим файл dog.js с таким кодом:
Це хороший спосіб розвитку вашої кар'єри в IT-індустрії. Після проходження курсу Mate гарантує вам офер мрії.
// Создать пса var dog = < name: "Ritchie Blaсkdog", species: "dog", breed: "hellhound", speak: function () < return "Woof! Woof-woof-woof!"; >>;
С помощью Deno можно выполнять файлы TypeScript прямо в консоли. Добавим в файл dog.ts такую строку:
console.log(`$ says $`)
deno run dog.ts
И получим результат:
![]()
5. Особенности TypeScript
Объявление типов
В TypeScript можно объявить переменную четырьмя способами: с указанием типа и значения, с указанием только типа, с указанием только значения — и без типа, и без значения.
// Объявить тип и значение в одном операторе. var amount:number = 10000.00; /* Объявить тип, не присваивая значения. По умолчанию переменной будет присвоено значение undefined. */ var amount:number; /* Объявить значение, не задавая тип. Переменная получит тип присвоенного значения. */ var amount = 10000.00; /* Не указывать ни типа, ни значения. В этом случае переменная получит тип any и значение undefined. */ var amount;
После того как объявлен тип переменной, ей невозможно присвоить значение другого типа.

Тип остается тем же, который был задан
Но если, все-таки, необходимо изменить тип переменной, то можно воспользоваться утверждением типа ( type assertion ). Для этого нужно заключить требуемый тип в символы < >и поместить перед переменной или выражением.
var x = '1' var y:number = x console.log(typeof(y))
Приведенный выше код выведет в консоль строку « string ».
Рассмотрим типы данных, определенные в TypeScript.
Any
Тип any — это надтип для всех типов в TypeScript, динамический тип. Его использование подразумевает отказ от проверки типа переменной.
Встроенные типы
В приведенной ниже таблице перечислены встроенные типы TypeScript и даны их описания.
| Ключевое слово | Описание |
| number | 64-разрядные числовые значения с плавающей точкой. Используются для представления как целых, так и дробных чисел. |
| string | Последовательность символов Unicode . |
| boolean | Логическое значение: true или false |
| void | Тип возврата для функций, которые не возвращают значения |
| null | Явное указание отсутствия значения объекта. |
| undefined | Значение, присваиваемое всем переменным до их инициализации |
Пользовательские типы
В приведенной ниже таблице перечислены пользовательские типы TypeScript и даны их описания.
Литералы типов
Типами в TypeScript могут быть не только строки вообще ( string ) или числа вообще ( number ). Конкретное число или конкретная строка тоже может быть типом. Например, типом может быть « 8 », а может быть « красный ». Если переменная объявлена с литералом типа, то она не может принимать никакие другие значения.
Рассмотрим это на примере кода:
let red:"красный" = "красный"; red = "красный"; red = "зеленый";
При попытке его скомпилировать получим ошибку:

И для чего же нужна переменная с уникальным типом и единственным значением?
Сама по себе она не особо полезна. Дело в применении.
Например, в CSS есть свойства, которые принимают лишь несколько определенных значений. Свойство alignment может принимать значения left, right или center .
Если объединить литералы типов left, right и center (которые объявлены как строки) в union , то получим двойную пользу:
- будут приниматься лишь допустимые значения;
- они будут передаваться в виде строк, которые можно вставить в код (не нужно ветвление, чтобы сопоставить значения из объединения со строковыми значениями).
Наглядно это будет выглядеть так:
function setText(msg: string, alignment: "left" | "right" | "center") < // . >setText("Готово!", "left"); setText("Ошибка!", "bottom");
Компилятор не дает ошибиться:

Шаблоны литералов типов
Шаблоны — это надстройка над литералами типов. Благодаря объединениям они могут разворачиваться во множество строк без необходимости в циклах.
В шаблонах литеральных типов используется тот же синтаксис, что и в шаблонах литералов строк JavaScript, но с указанием типов, а не переменных.

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

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

То есть, весь список значений для этого типа будет таким:
type cardsByCurrencies = "USD_prepaid_visa" | "USD_prepaid_mastercard" | "USD_credit_visa" | "USD_credit_mastercard" | "EUR_prepaid_visa" | "EUR_prepaid_mastercard" | "EUR_credit_visa" | "EUR_credit_mastercard"
Необходимо также заметить, что и в литералах типов, равно как и в шаблонах, может использоваться вывод типов. Подробнее см. в документации.
Классы
TypeScript — это объектно-ориентированный JavaScript. Он поддерживает классы, интерфейсы и т. п. TypeScript получил поддержку классов от ES6.
Объявление класса
Для объявления класса используется ключевое слово class .
class User < // Область видимости класса >
Приведенный выше код компилируется в такой код JavaScript.
var User = /** @class */ (function () < function User() < >return User; >());
Приведем пример объявления класса с полями, конструктором и функцией.
class Person < // Поле firstName: string; lastName: string; // Конструктор constructor(firstName:string, lastName: string) < this.firstName = firstName; this.lastName = lastName; >// Функция logFullName(): void < console.log(`$$`); > >
На JavaScript этот код будет выглядеть так:
var Person = /** @class */ (function () < // Конструктор function Person(firstName, lastName) < this.firstName = firstName; this.lastName = lastName; >// Функция Person.prototype.logFullName = function () < console.log("".concat(this.firstName, " ").concat(this.lastName)); >; return Person; >());
Создание экземпляра класса
Экземпляр класса в TypeScript создается с помощью ключевого слова new .
var p = new Person('Mickey', 'Mouse');
Доступ к полям и функциям
Для обращения к полям и функциям используется запись с точкой.
p.firstName; p.logFullName();
Сведем все воедино:
class Person < // Поле firstName: string; lastName: string; // Конструктор constructor(firstName:string, lastName: string) < this.firstName = firstName; this.lastName = lastName; >// Функция logFullName(): void < console.log(`$$`); > > var p = new Person('Mickey', 'Mouse'); console.log(`Имя: $`); console.log(`Фамилия: $`); console.log('Полное имя:'); p.logFullName();
var Person = /** @class */ (function () < // Конструктор function Person(firstName, lastName) < this.firstName = firstName; this.lastName = lastName; >// Функция Person.prototype.logFullName = function () < console.log("".concat(this.firstName, " ").concat(this.lastName)); >; return Person; >()); var p = new Person('Mickey', 'Mouse'); console.log("\u0418\u043C\u044F: ".concat(p.firstName)); console.log("\u0424\u0430\u043C\u0438\u043B\u0438\u044F: ".concat(p.lastName)); console.log('Полное имя:'); p.logFullName();

Как видите, если вы знакомы с ООП и JavaScript, все довольно просто.
Пространства имен
Пространства имен — это способ логической группировки взаимосвязанного кода. Это встроенная возможность TypeScript. В JavaScript пространства имен отсутствуют, поэтому может возникнуть конфликт имен, если в нескольких файлах встретятся переменные с одним и тем же именем.
Пример
Конфликт имен в JavaScript
var text = "Text from scr1.js"; console.log(text);
var text = "Text from scr2.js"; console.log(text);
Первым импортируется файл scr1.js , вторым — scr2.js , поэтому вложенный сценарий, объявленный после импорта двух внешних, получит переменную text со значением, которое присвоено в scr2.js .

TypeScript устраняет этот недостаток за счет пространств имен. Термин «пространство имен» введен в TypeScript 1.5. До этой версии в TypeScript были внутренние и внешние модули. Теперь внутренние модули называются пространствами имен, а внешние — просто модулями.
Для определения пространства имен используется ключевое слово namespace. Классы и интерфейсы, к которым будут обращаться извне модуля, необходимо пометить ключевым словом export .
namespace MyNameSpaceName < export MyClassName < >export IMyInterfaceName < >>
Для доступа к классу или интерфейсу, которые находятся в другом пространстве имен, используется запись с точкой: namespaceName.className .
Если пространство имен находится в другом файле, то необходимо указать ссылку на этот файл, используя запись с тремя косыми чертами:
Объявим переменные с одним и тем же именем в разных пространствах имен.
namespace Animals
namespace People
/// /// console.log(People.count); console.log(Animals.count);
Скомпилируем эти файлы в код JavaScript. Для этого передадим команде tsc параметр --outFile , за которым указывается имя целевого файла JavaScript, а затем перечисляются файлы TypeScript.
npx tsc --outFile howmany.js howmany.ts animals.ts people.ts
Получим следующий код JavaScript:
var People; (function (People) < People.count = 3; >)(People || (People = <>)); var Animals; (function (Animals) < Animals.count = 12; >)(Animals || (Animals = <>)); /// /// console.log(People.count); console.log(Animals.count);
Соответственно, результаты вывода значения count будут разными: 3 и 12.
У пространств имен в TypeScript есть такие возможности:
- пространство имен можно расположить в отдельном файле, который подключается с помощью ссылки;
- пространства имен можно вкладывать друг в друга. Вложенные пространства имен помечаются ключевым словом export;
- в одном файле можно объявить несколько пространств имен;
- пространство имен можно разбить на несколько файлов, приводя ссылку на основной файл в каждом дополнительном;
- чтобы не набирать долгие пути к классам, можно объявить псевдоним импортируемого класса. Например, если у нас есть пространство имен Data , в которое вложено пространство имен Animal с классом Dog , то псевдоним можно объявить так: import dog = Data.Animal.Dog . Тогда можно будет обращаться к полям и методам этого класса через префикс dog .
Модули
Модули помогают упорядочить код, написанный на TypeScript. До TypeScript 1.5 модули делились на внутренние и внешние. С версии 1.5 внутренние модули называются пространствами имен, а внешние — собственно модулями.
Модулем считается любой файл с импортом или экспортом на высшем уровне. Модули выполняются в собственной области видимости.
Файл без импорта и экспорта на высшем уровне считается сценарием, чье содержимое доступно в глобальной области видимости.
Чтобы интерфейс, класс, переменную или функцию можно было использовать в другом файле, перед объявлением добавляется ключевое слово export (как это делалось в примере с пространствами имен). Но вместо того, чтобы добавлять его к каждому объявлению, можно использовать более простую и наглядную запись, объединяя классы, интерфейсы и т. п.:
export < Dog, move >;
Модуль импортируется с помощью ключевого слова import :
import < Dog, move >from "./myfile.js";
При импорте можно указывать псевдонимы с помощью ключевого слова as (его также можно использовать для создания псевдонима при экспорте):
import < Dog, move as myMove >from "./myfile.js";
Можно импортировать модуль целиком в одно пространство имен:
import * as mymodule from "./myfile.js";
В конце концов, можно указать один класс как экспортируемый по умолчанию. Для этого после слова export указывается слово default . При этом данный класс будет импортироваться по умолчанию, даже если в операторе импорта указан псевдоним.
Совместимость с JavaScript. Окружение (Ambient)
JavaScript — это подмножество TypeScript. Казалось бы, если в сценарии TS есть код JS, то компилятор TypeScript примет его без проблем. Это справедливо, если код JavaScript написан с учетом безопасности с точки зрения типов.
Другое дело, если вы используете большие библиотеки JS или сторонние, например jQuery, Node.js или AngularJS (именно AngularJS, потому что библиотека Angular написана на TypeScript). Чтобы обеспечить безопасность типов и ввод с автодополнением ( intellisense ), TypeScript-программисту придется приложить нечеловеческие усилия.
Объявления окружения ( ambient ) помогают беспроблемно встраивать библиотеки JS в TypeScript. Определения окружений по соглашению помещаются в файл определения типов в расширением d.ts , например ThirdPartyLib.d.ts . Этот файл не компилируется в JavaScript.
Переменные и модули определяются так:
declare module Module_Name < export class SomeClass< doSmth(times:number) : number; >>
В файле окружения нет реализации. В нем объявляются типы. Затем они включаются в файл TypeScript следующим образом:
В результате при попытке передать функции doSmth строковое значение будет выдано сообщение об ошибке — еще во время компиляции.
Таким образом можно состыковывать сторонние библиотеки с кодом TypeScript и не беспокоиться о том, что код поведет себя непредсказуемо.
6. Пишем приложение на TypeScript
Создадим приложение для предварительного просмотра сообщений. Сообщения будут делиться на три вида: информационные, предупреждения и сообщения об ошибке. В зависимости от выбранного типа сообщения меняется его стиль.

Создадим файл с полями для указания заголовка, текста и типа сообщения, а также с родительским элементом для его просмотра. Просмотр измененного сообщения будет доступен по нажатию кнопки.
Сообщения Сообщения
Заголовок
Текст
Тип
ЗаголовокТекст сообщенияНастроим внешний вид элементов. Стиль элементов будет состоять из двух частей: типа ( error|warning|info ) и назначения: заголовка и тела сообщения.
body < font-family: Verdana, sans-serif; color: #336699; >div < margin: 5px; padding: 5px; >.param < width:300px; display: inline-block; float: left; background-color: #336699; color: #ffffff; border-radius: 10px; >.result < display: inline-block; float: left; width: 300px; background-color: #ffffff; color: #336699; border: 1px solid #003366; border-radius: 10px; >.message < border: 1px solid; border-radius: 10px; padding: 0px; >.header < font-weight: bold; font-size: larger; >.body < font-style: italic; >.info < color: #009900; border-color: #009900; >.warning < color: #FF8C00; border-color: #FF8C00; >.error
По нажатию кнопки будем менять весь стиль элемента разом. Поскольку из скрипта можно менять только его отдельные свойства, заменим весь innerHTML элемента result . Для каждого элемента укажем класс, подставив литеральный тип перед идентификатором класса назначения элемента.
// Типы сообщений помещаем в объединение type msgTypes = "info" | "warning" | "error"; // Части сообщения тоже помещаем в объединение type msgParts = "header" | "body"; class Message < header: string; body: string; type: msgTypes; constructor(header: string, body: string, type: msgTypes) < this.header = header; this.body = body; this.type = type; >insert(resultElement: HTMLElement) : void < // Поскольку тип определен как строковой литерал, // включаем его в строку без проблем resultElement.innerHTML = `$"); >; return Message; >()); function updateMessage() < // Получаем значение элементов в соответствии с типами var header = document.getElementById("header").value; var body = document.getElementById("body").value; var type = document.getElementById("type").value; // Настраиваем сообщение var msg = new Message(header, body, type); msg.insert(document.getElementById("result")); >$`; > > function updateMessage() < // Получаем значения элементов в соответствии с типами const header = (document.getElementById("header") as HTMLInputElement).value; const body = (document.getElementById("body") as HTMLInputElement).value; const type = (document.getElementById("type") as HTMLSelectElement).value; // Настраиваем сообщение let msg = new Message(header, body, type as msgTypes); msg.insert(document.getElementById("result")); >Это приложение не столько полезно с точки зрения функциональности, сколько наглядно с точки зрения знакомства с некоторыми рассмотренными здесь возможностями TypeScript: типами, объединениями, шаблонами литералов типов.
После компиляции получим такой код:
var Message = /** @class */ (function () < function Message(header, body, type) < this.header = header; this.body = body; this.type = type; >Message.prototype.insert = function (resultElement) < resultElement.innerHTML = "").concat(this.header, "\n").concat(this.body, "\n7. Преимущества и недостатки TypeScript
Преимущества TypeScript составляют, среди прочих, его следующие особенности:
- возможность выявлять ошибки еще в редакторе кода и во время компиляции;
- поддержка ООП;
- удобная система типов;
- пригодность для разработки крупных проектов.
Недостатки TypeScript, на наш взгляд, таковы:
- снижает скорость разработки за счет необходимости указания типов;
- компиляция существенно замедляет сборку, особенно если речь идет о больших проектах;
- необходимость самостоятельно прописывать сигнатуры для внешних библиотек;
- порог входа повышен: в TypeScript столько нововведений, что его освоение можно сравнить с изучением нового языка.
Итоги
TypeScript позволяет избегать ошибок уже в начале разработки проекта, что избавляет от непредвиденных затрат. TypeScript можно представить себе как безопасное расширение, своего рода апгрейд для старого-доброго Javascript, программирование в котором давно уже имеет довольно недобрую репутацию.
В TypeScript реализовано много интересных и полезных возможностей. Поэтому над ним придется помучиться, зато вы освоите язык, популярность которого растет: в 2021 году он обогнал C++ и занял 6-е место среди языков программирования, используемых в работе.
В любом случае знакомство с этим языком идет на пользу, и если вдруг появится возможность работать над крупным фронтенд-проектом на TypeScript, вы будете заранее готовы.
Курс Аналітик даних.
Протягом 4 місяців ви вивчите повний набір інструментів для аналізу даних та отримаєте можливість працевлаштування в Laba Group.
Знакомимся с TypeScript
Во многих отношениях полезно судить о TypeScript по его достоинствам. Спецификация языка TypeScript описывает его как «синтаксический сахар для JavaScript». Это верно и, по-видимому, важно для охвата целевой аудитории этого языка — разработчиков клиентского ПО, в настоящее время использующих JavaScript.
И вы действительно должны понимать JavaScript, прежде чем сможете разобраться в TypeScript. По сути, в спецификации языка (см. bit.ly/1xH1m5B) конструкции TypeScript часто описываются в терминах получаемого JavaScript-кода. Но не менее полезно рассматривать TypeScript как отдельный язык, имеющий некую общность с JavaScript.
Например, как и C#, TypeScript является языком с типизированными данными, что дает вам поддержку IntelliSense и проверки при компиляции, не говоря уж о других средствах. Как и C#, TypeScript включает обобщения и лямбда-выражения (или их эквиваленты).
Но TypeScript, конечно, не является C#. Понимание уникальных качеств TypeScript не менее важно понимания того, что именно у TypeScript общего с используемым вами в настоящее время языком для программирования на серверной стороне. Система типов TypeScript отличается (в сторону упрощения) от C#. TypeScript уникальным образом использует заложенные в него знания других объектных моделей и реализует наследование иначе, чем C#. А поскольку TypeScript компилируется в JavaScript, TypeScript имеет много общих основополагающих принципов с JavaScript (в отличие от C#).
Остается вопрос: «На каком языке лучше писать клиентский код — на TypeScript или JavaScript?».
TypeScript — язык с типизацией данных
В TypeScript не так уж много встроенных типов данных, которые можно использовать в объявлениях переменных: только string, number и boolean. Эти три типа являются подтипами типа any (что тоже можно использовать при объявлении переменных). Вы можете присваивать переменным, объявленным с помощью четырех типов, такие типы, как null или undefined, или проверять их на равенство этим типам. Кроме того, можно объявлять методы как void, указывая, что они не возвращают значение.
В этом примере переменная объявляется как string:
var name: string;Вы можете расширить эту простую систему типов перечислимыми значениями и четырьмя видами объектных типов: interface, class, array и function. Так, в следующем коде определяется интерфейс (один вид объектного типа) с именем ICustomerShort. Этот интерфейс включает два члена: свойство Id и метод CalculateDiscount:
interface ICustomerShortКак и в C#, вы можете использовать интерфейсы при объявлении переменных и возвращаемых типов. ниже объявляется переменная cs с типом ICustomerShort:
var cs: ICustomerShort;Вы также можете определять объектные типы как классы, которые в отличие от интерфейсов могут содержать исполняемый код. В этом примере определяется класс CustomerShort с одним свойством и одним методом:
class CustomerShort < FullName: string; UpdateStatus( status: string ): string < . манипулируем переменной status. return status; >>Подобно более новым версиям C# при определении свойство не обязательно предоставлять код реализации. Достаточно простого объявления имени и типа. Классы могут реализовать один или более интерфейсов, как показано на рис. 1, где мой интерфейс ICustomerShort вместе с его свойством добавляется в мой класс CustomerShort.
Рис. 1. Добавление интерфейса в класс
class CustomerShort implements ICustomerShort < Id: number; FullName: string; UpdateStatus(status: string): string < . манипулируем переменной status. return status; >CalculateDiscount(): number < var discAmount: number; . calculate discAmount. return discAmount; >>Как и C#, TypeScript является языком с типизированными данными.
Как видно на рис. 1, синтаксис реализации интерфейса в TypeScript столь же прост, как и в C#. Чтобы реализовать члены интерфейса, вы лишь добавляете члены с тем же именем вместо связывания имени интерфейса с релевантными членами класса. В этом примере я добавил Id и CalculateDiscount в класс для реализации ICustomerShort. TypeScript также позволяет использовать литералы объектного типа. В следующем коде переменная cst присваивается объектному литералу, содержащему одно свойство и один метод:
var csl = < Age: 61, HaveBirthday(): number < return this.Age++; >>;В этом примере импользуется объектный тип, чтобы указать возвращаемое значение метода UpdateStatus:
UpdateStatus( status: string ): < status: string; valid: boolean >< return ; >Помимо объектных типов (class, interface, literal и array), вы также можете определять типы функций, которые описывают сигнатуру функции. В следующем коде переписан CalculateDiscount из моего класса CustomerShort для приема единственного параметра с именем discountAmount:
interface ICustomerShort < Id: number; CalculateDiscount( discountAmount: ( discountClass: string, multipleDiscount: boolean ) =>number): number >Этот параметр определен с применением типа функции, который принимает два параметра (string и boolean) и возвращает тип number. Если вы разработчик на C#, то, возможно, находите этот синтаксис очень похожим на лямбда-выражение.
Класс, реализующий этот интерфейс, выглядел бы так, как показано на рис. 2.
Рис. 2. Этот класс реализует соответствующий интерфейс
class CustomerShort implements ICustomerShort < Id: number; FullName: string; CalculateDiscount( discountedAmount: ( discountClass: string, multipleDiscounts: boolean ) =>number ): number < var discAmount: number; . calculate discAmount. return discAmount; >>Как и недавние версии C#, TypeScript также логически распознает тип данных переменной по значению, которым инициализируется эта переменная. Ниже TypeScript предположит, что переменная myCust имеет тип CustomerShort:
var myCust= new CustomerShort(); myCust.FullName = "Peter Vogel";Как и в C#, можно объявлять переменные, используя интерфейс, а затем присваивая переменную объекту, который реализует этот интерфейс:
var cs: ICustomerShort; cs = new CustomerShort(); cs.Id = 11; cs.FullName = "Peter Vogel";Наконец, вы также можете использовать параметры типа (что подозрительно напоминает обобщения в C#), чтобы позволить вызывающему коду указать нужный тип данных. В этом примере я даю возможность коду, создающему класс, задавать тип данных свойства Id:
class CustomerTyped
А этот код указывает тип данных свойства Id как string до его использования:
var cst: CustomerTyped; cst = new CustomerTyped(); cst.Id = "A123";Чтобы изолировать классы, интерфейсы и другие открытые члены и избегать конфликтов имен, можно объявлять эти конструкции внутри модулей, напоминающих пространства имен в C#. Вам придется пометить те элементы, которые вы хотите сделать доступными другим модулям, ключевым словом export. Модуль на рис. 3 экспортирует два интерфейса и класс.
Рис. 3. Экспорт двух интерфейсов и одного класса
module TypeScriptSample < export interface ICustomerDTO < Id: number; >export interface ICustomerShort extends ICustomerDTO < FullName: string; >export class CustomerShort implements ICustomerShortЧтобы использовать экспортируемые компоненты, имя компонента можно дополнять именем модуля, как в следующем примере:
var cs: TypeScriptSample.CustomerShort;Или использовать ключевое слово import в TypeScript, чтобы указать сокращенное имя модуля:
import tss = TypeScriptSample; . var cs:tss.CustomerShort;Гибкая типизация данных
Все это должно выглядеть знакомым, если вы программист на C#, кроме, возможно, обратного порядка объявления переменных (сначала имя переменной, потом тип данных) и объектных литералов. Однако практически вся типизация данных в TypeScript не является обязательной. В спецификации типы данных описываются как аннотации. Если вы опускаете типы данных (а TypeScript не удается логически распознать их), то они считаются по умолчанию типом any.
TypeScript также не требует строгого совпадения типов данных (datatype matching). Для определения совместимости TypeScript использует то, что в спецификации называют структурным порождением подтипов (structural subtyping). Это подобно тому, что часто называют утиной типизацией (duck typing). В TypeScript два класса считаются идентичными, если они имеют члены с одинаковыми типами. Вот класс CustomerShort, который реализует интерфейс ICustomerShort:
interface ICustomerShort < Id: number; FullName: string; >class CustomerShort implements ICustomerShortА вот класс CustomerDeviant, внешне похожий на мой класс CustomerShort:
class CustomerDeviantПрактически вся типизация данных в TypeScript не является обязательной.
Благодаря структурному порождению подтипов я могу применять CustomerDevient с переменными, определенными с помощью моего класса CustomerShort или интерфейса ICustomerShort. В следующих примерах CustomerDeviant используется взаимозаменяемо с переменными, объявленными как CustomerShort или ICustomerShort:
var cs: CustomerShort; cs = new CustomerDeviant cs.Id = 11; var csi: ICustomerShort; csi = new CustomerDeviant csi.FullName = "Peter Vogel";Такая гибкость позволяет присваивать объектные литералы в TypeScript переменным, объявленным как классы или интерфейсы, — при условии, что они структурно совместимы, как, например, здесь:
var cs: CustomerShort; cs = var csi: ICustomerShort; csi =Это уводит нас в специфические для TypeScript средства вроде видимых типов (apparent types), надтипов и подтипов, о чем я не стану здесь рассказывать. Эти средства позволили бы, например, CustomerDeviant иметь члены, отсутствующие в CustomerShort, и при этом не вызывать никаких ошибок в моем примере кода.
В TypeScript есть наследование классов
Спецификация TypeScript описывает язык как реализующий «цепочки прототипов шаблонов классов [using], что позволяет реализовать множество вариаций объектно-ориентированных механизмов наследования». На практике это означает, что TypeScript является не только типизированным, но и, по сути, объектно-ориентированным языком.
Аналогично тому, как интерфейс в C# может наследовать от базового интерфейса, интерфейс в TypeScript может расширять другой интерфейс, даже если этот другой интерфейс определен в другом модуле. В следующем примере я расширяю интерфейс ICustomerShort, чтобы создать новый интерфейс с именем ICustomerLong:
interface ICustomerShort < Id: number; >interface ICustomerLong extends ICustomerShortУ интерфейса ICustomerLong будут два члена: FullName и Id. В новом интерфейсе члены исходного интерфейса появляются первыми. Поэтому мой интерфейс ICustomerLong эквивалентен интерфейсу:
interface ICustomerLongPseudoКлассу, который реализует ICustomerLong, понадобятся оба свойства:
class CustomerLong implements ICustomerLongКлассы могут расширять другие классы точно так же, как в случае интерфейсов. Класс на рис. 4 расширяет CustomerShort и добавляет новое свойство в определение. Он использует явные аксессоры get и set для определения свойств (хоть и не особо полезным способом).
Рис. 4. Свойства, определенные с помощью аксессоров get и set
class CustomerShort < Id: number; >class CustomerLong extends CustomerLong < private id: number; private fullName: string; get Id(): number < return this.id >set Id( value: number ) < this.id = value; >get FullName(): string < return this.fullName; >set FullName( value: string ) < this.fullName = value; >>TypeScript вводит в действие правила доступа к внутренним полям (вроде id и fullName) через ссылку на класс (this). Классы также могут иметь функции-конструкторы, которые включают только что внесенное в C# средство: автоматическое определение полей. Функция-конструктор в TypeScript-классе должна называться constructor, а ее открытые параметры автоматически определяются как свойства и инициализируются значениями, передаваемыми в этих параметрах. В следующем примере конструктор принимает один параметр — Company типа string:
export class CustomerShort implements ICustomerShortПоскольку параметр Company определен как открытый, класс также получает открытое свойство Company, инициализируемое значением, переданным в конструктор. Благодаря этой функциональности в следующем примере переменной comp будет присвоено «PH&VIS»:
var css: CustomerShort; css = new CustomerShort( "PH&VIS" ); var comp = css.Company;Объявление параметра конструктора закрытым создает внутреннее свойство, доступной только из кода членов класса через ключевое слово this. Если параметр не объявлен как открытый или закрытый, никакое свойство не генерируется.
TypeScript является не только типизированным, но и, по сути, объектно-ориентированным языком.
В вашем классе должен быть конструктор. Как и в C#, если вы не предоставляете конструктор, он создается за вас. Если ваш класс расширяет другой, любой создаваемый вами конструктор должен включать вызов super. Это вызывает конструктор в расширяемом классе. Пример ниже включает конструктор с вызовом super, который передает параметры конструктору базового класса:
class MyBaseClass < constructor(public x: number, public y: number ) < >> class MyDerivedClass extends MyBaseClass < constructor() < super(2,1); >>Наследование в TypeScript работает по-другому
Все это выглядит вам знакомым, если вы программист на C#, за исключением некоторых странных ключевых слов (вроде extends). Но расширение класса или интерфейса — не совсем то же самое, что механизмы наследования в C#. В спецификации TypeScript используются обычные термины для расширяемого класса (базовый класс) и класса, расширяющего его (производный класс). Однако эта спецификация, например, ссылается на спецификацию наследования класса вместо употребления слова «наследование».
Прежде всего в TypeScript меньше вариантов, чем в C#, когда дело доходит до определения базовых классов. Вы не можете объявить класс или члены как не подлежащие перегрузке, как абстрактные или виртуальные (хотя интерфейсы обеспечивают во многом ту же функциональность, что и виртуальные базовые классы).
Нет никакого способа предотвратить наследование от некоторых членов. Производный класс наследует все члены базового класса, включая открытые и закрытые члены (все открытые члены базового класса могут быть перегружены, а закрытые — нет). Чтобы переопределить открытый член, просто определите член в производном классе с той же сигнатурой. Хотя вы можете пользоваться ключевым словом super для доступа к открытому методу из производного класса, это ключевое слово не позволяет обращаться к какому-либо свойству в базовом классе (но свойство можно переопределить).
TypeScript позволяет дополнять интерфейс простым объявлением интерфейса с идентичным именем и новыми членами. Это дает возможность расширять существующий JavaScript-код без создания нового именованного типа. В примере на рис. 5 определяется интерфейс ICustomerMerge через два разных определения интерфейса, а затем этот интерфейс реализуется в классе.
Рис. 5. Интерфейс ICustomerMerge определен через два определения интерфейса
interface ICustomerMerge < MiddleName: string; >interface ICustomerMerge < Id: number; >class CustomerMerge implements ICustomerMergeКлассы могут расширять другие классы, но не интерфейсы. В TypeScript интерфейсы могут расширять и классы, но только таким способом, который использует наследование. Расширяя класс, интерфейс включает все члены класса (открытые и закрытые), но без реализаций в классе. На рис. 6 интерфейс ICustomer получит закрытый член id, открытый член Id и открытый член MiddleName.
Рис. 6. Расширенный класс со всеми членами
class Customer < private id: number; get Id(): number < return this.id >set Id( value: number ) < this.id = value; >> interface ICustomer extends CustomerИнтерфейс ICustomer имеет существенное ограничение: вы можете использовать его только с классами, расширяющими тот же класс, который расширен интерфейсом (в данном случае это класс Customer). TypeScript требует, чтобы вы включали закрытые члены в интерфейс, который должен наследоваться от класса, расширяемого этим интерфейсом, а не заново реализовали их в производном классе. Новому классу, использующему интерфейс ICustomer понадобилось бы, например, предоставить реализацию для MiddleName (поскольку в интерфейсе он лишь указан). Разработчик, использующий ICustomer, мог бы выбрать либо наследование, либо переопределение открытых методов класса Customer, но не смог бы переопределить закрытый член id.
Классы могут расширять другие классы точно так же, как в случае интерфейсов.
В следующем примере показан класс (с именем NewCustomer), который реализует интерфейс ICustomer и расширяет класс Customer так, как это требуется. В этом примере NewCustomer наследует реализацию Id от Customer и предоставляет реализацию для MiddleName:
class NewCustomer extends Customer implements ICustomerЭта комбинация интерфейсов, классов, реализации и расширения дает возможность определяемым вами классам контролировать расширение классов, определенных в других объектных моделях (подробности см. в разделе 7.3 языковой спецификации «Interfaces Extending Classes»). В сочетании со способностью TypeScript использовать информацию о других JavaScript-библиотеках это позволяет писать TypeScript-код, который работает с объектами, определенными в этих библиотеках.
TypeScript знает о ваших библиотеках
Помимо классов и интерфейсов, определенных в вашем приложении, вы можете предоставить TypeScript информацию о других объектных библиотеках. Это обрабатывается с помощью ключевого слова declare в TypeScript. Оно создает то, что в спецификации называют окружающими объявлениями (ambient declarations). Возможно, вам никогда не придется самостоятельно использовать ключевое слово declare, потому что вы можете найти файлы определений для большинства JavaScript-библиотек на сайте DefinitelyTyped (definitelytyped.org). Через эти файлы определений TypeScript может фактически «читать документацию» по библиотекам, с которыми вам нужно работать.
«Чтение документации», конечно, подразумевает, что вы получаете поддержку IntelliSense по типизированным данным и проверки этапа компиляции при использовании объектов, образующих библиотеку. Кроме того, это позволяет TypeScript при определенных обстоятельствах логически распознавать тип переменной по контексту, в котором она применяется. Благодаря файлу определений lib.d.ts, включенному в TypeScript, этот язык предполагает, что переменная anchor имеет тип HTMLAnchorElement в следующем коде:
var anchor = document.createElement( "a" );Файл определений указывает, что это результат, возвращаемый методом createElement, когда ему передается строка «a». Зная, что anchor — это HTMLAnchorElement, TypeScript знает, что переменная anchor будет поддерживать, к примеру, метод addEventListener.
Логическое распознавание типов данных в TypeScript также работает с типами параметров. Например, метод addEventListener принимает два параметра. Второй из них — это функция, через которую addEventListener передает объект типа PointerEvent. TypeScript известно это, и он поддерживает доступ к свойству cancelBubble класса PointerEvent внутри функции:
span.addEventListener("pointerenter", function ( e )Аналогично тому, как lib.d.ts предоставляет информацию о HTML DOM, файлы определений для других JavaScript-библиотек обеспечивают сходную функциональность. После добавления файла backbone.d.ts в свой проект я могу, например, объявить класс, который расширяет класс Backbone Model и реализует собственный интерфейс следующим кодом::
class CustomerShort extends bb.Model implements ICustomerShortЕсли вас интересуют подробности того, как использовать TypeScript с Backbone и Knockout, почитайте статьи в моей рубрике «Practical TypeScript» по ссылке bit.ly/1BRh8NJ. В новом году я буду рассматривать детали применения TypeScript с Angular.
TypeScript — нечто большее, чем вы видели в этой статье. В TypeScript версии 1.3 планируется включить тип данных union (например, для поддержки функций, которые возвращают список определенных типов) и кортежи (tuples). Группа TypeScript сотрудничает с другими группами, применяющими типизацию данных в JavaScript (Flow и Angular), чтобы гарантировать работоспособность TypeScript с максимально широким спектром JavaScript-библиотек.
Если вам нужно сделать что-то, что JavaScript позволяет, а TypeScript — нет, вы всегда можете интегрировать свой JavaScript-код, поскольку TypeScript является надмножеством JavaScript. Так что вопрос остается открытым: какой из этих языков вы предпочли бы использовать для написания клиентского кода?
Питер Вогел (Peter Vogel) — один из руководителей PH&V Information Services, специализируется в веб-разработке, эксперт по SOA, разработке клиентского ПО и дизайна UI. В число клиентов PH&V входят Canadian Imperial Bank of Commerce, Volvo и Microsoft. Он также занимается обучением и пишет учебные курсы для Learning Tree International. Ведет рубрику Practical .NET на сайте VisualStudioMagazine.com.
Выражаю благодарность за рецензирование статьи эксперту Microsoft Райену Кевено (Ryan Cavanaugh).
Предыдущая запись
