TypeScript
Почему Typescript?
Typescript - это не "еще один язык программирования". В двух словах - это просто надмножество Javascript (ES6) с опциональной статической типизацией. Именно эти две особенности позволяют создавать масштабные приложения, сохраняя качество и упрощая разработку.
Начать просто.
Для того, чтобы начать писать на Typescript, не нужно сразу перечитывать всю документацию. Достаточно установить или интегрировать в существующее приложение и можно сразу писать на том же Javascript'е, к которому вы привыкли. Потому что Typescript - это всего лишь расширение существующего стандарта. Постепенно добавляйте типы и другие удобные особенности в свое приложение по мере изучения руководства и вы почувствуете его преимущество.
Базовые типы
Typescript является языком со статической типизацией. Тип не может быть изменен в ходе выполнения программы. Это позволяет снизить большое количество ошибок и выявить многие из них еще на этапе компиляции.
В Typescript есть несколько простых типов данных: numbers (числа), strings (строки), structures (структуры), boolean (логический). Он поддерживает все типы, которые есть в Javascript, дополняя удобным типом перечислений (enum).
Boolean
Наиболее базовым типом является логический ture/false, который в Javascript и Typescript называется boolean.
let isDone: boolean = false;
Number
Как и в Javascript, тип numbers в Typescript являются числом с плавающей точкой. Кроме десятичного и шестнадцатиричного формата, поддерживаются бинарный и восьмеричный, введенные в ECMAScript 2015.
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
Еще одна важная часть программ в веб-страницах и серверах это текстовые данные. Как и в других языках, в Typescript используется то же обозначение "string" для таких данных. Как и Javascript, в Typescript используются двойные (") или одинарные (') кавычки для обрамления текстовых данных.
let name: string = "bob";
name = 'smith';
Array
TypeScript, как и JavaScript, имеет массивы значений. Тип массива может быть определен одним из двух способов. Первый - обозначать тип элементов массива перед [].
Второй способ - использовать обобщение Array<elemType.
const list: number[] = [1, 2, 3];
const users: string[] = ['Sam', 'John'];
const list: Array<number> = [1, 2, 3];
const users: Array<string> = ['Sam', 'John'];
Tuple
Тип Tuple дает вам возможность объявить массив с известным фиксированным количеством элементов, которые не обязаны быть одного типа. Например, вы хотите иметь значение Tuple как пару "строка" и "число":
// Объявление типа tuple
let x: [string, number];
// Его инициализация
x = ['hello', 10]; // OK
// Некорректная инициализация вызовет ошибку
x = [10, 'hello']; // Error
Enum (Перечисления)
Как и в языках подобных C#, тип enum - это более удобный способ задания понятных имен набору численных значений.
enum Color {Red, Green, Blue};
let green: Color = Color.Green; // 1
let red: Color = Color.Red; // 0
По умолчанию перечисления начинаются с 0. Вы можете изменить это путем прямого указания значения для одного из членов перечисления. Например, мы можем начать предыдущий пример с 1 вместо 0, или даже задать значения для всех членов:
enum Color {Red = 1, Green, Blue};
let green: Color = Color.Green; // 2
let red: Color = Color.Red; // 1
enum Colors {Red = 1, Green = 5, Blue = 8};
let c: Color = Color.Green; // 5
Any
Нам может потребоваться описать тип переменных, который мы не знаем, когда пишем наше приложение. Эти значения могут быть получены из динамического контента, например от пользователя или от сторонней библиотеки. В этих случаях мы хотим отключить проверку типов и позволить значениям пройти проверку на этапе компиляции. Чтобы это сделать, нужно использовать тип any:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // ok, это определенно boolean
Void
void это нечто противоположное any: отсутствие каких-либо типов. Чаще всего он используется в качестве возвращаемого типа функций, которые не возвращают никакого значения.
function warnUser(): void {
alert("This is my warning message");
}
Интерфейсы
Самый простой способ увидеть, как работают интерфейсы — начать с простого примера:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
// Мы можем переписать этот пример, на этот раз используя интерфейс для того,
// чтобы отразить необходимость наличия свойства label строкового типа:
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
Опциональные свойства.
Не все свойства интерфейса могут быть обязательными. Некоторые существуют только в определенных условиях, либо отсутствуют вообще. Такие необязательные (опциональные) свойства часто встречаются, к примеру, при передаче в функцию аргументов в виде объекта, в котором указаны всего несколько свойств.
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"}); // параметр width не передан, ошибки нет
Свойства только для чтения.
Некоторые свойства должны быть изменяемыми только в момент создания объекта. Вы можете указать это, добавив readonly перед его именем:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // ошибка! нельзя переписать readonly св-во
Функциональные типы.
Для того, чтобы описать функцию с помощью интерфейса, к нему добавляют сигнатуру вызова. Такая сигнатура выглядит как описание функции, в котором указаны только список аргументов и возвращаемый тип. Каждый параметр в списке должен иметь и имя, и тип.:
interface SearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: SearchFunc = function(source: string, subString: string) {
// типы параметров функции теперь можно не прописывать, они подтянутся автоматически
let result = source.search(subString);
if (result == -1) {
return false;
}
else {
return true;
}
}
Расширение интерфейсов.
Интерфейсы могут расширять друг друга, подобно классам. Это позволяет копировать члены одного интерфейса в другой, что дает больше гибкости при разделении интерфейсов на переиспользуемые компоненты.
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
const square: Square = {
color: "blue",
sideLength: 10,
};
interface PenStroke {
penWidth: number;
}
interface SquareExtended extends Square, PenStroke {
dotted?: boolean;
}
const squareExt: SquareExtended = {
color: "blue",
sideLength: 10,
penWidth: 5,
}
Функции
Добавление типов к функции.
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };
Добавлять типы можно к каждому параметру, а также и к самой функции, чтобы указать тип возвращаемого значения.
TypeScript умеет сам выводить тип возвращаемого значения, анализируя инструкции return, поэтому зачастую можно не указывать его явно.
Опциональные параметры и параметры по умолчанию .
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // сейчас все правильно
let result2 = buildName("Bob", "Adams", "Sr."); // ошибка, слишком много параметров
let result3 = buildName("Bob", "Adams"); // в самый раз
// Улучшим пример, указав для 2го параметра значение,
// которое он будет принимать, если пользователь пропустит его или передаст undefined.
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // пока что все правильно, возвращает "Bob Smith"
let result2 = buildName("Bob", undefined); // тоже работает и возвращает "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // ошибка, слишком много параметров
let result4 = buildName("Bob", "Adams"); // в самый раз
this - контекст выполнения функции в TypeScript
Параметр this — это "фальшивый" параметр, который идет первым в списке параметров функции:
function f(this: void) {
// Гарантировать, что в этой отдельной функции 'this' использовать нельзя
}
// пример посложнее
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// ВНИМАНИЕ: Сейчас функция явно указывает на то, что она должна вызываться на объекте типа Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Перегрузки функции
Чтобы типизировать функцию, которая возвращает объекты различных типов в зависимости от переданных аргументов, нужно указать для одной функции несколько типов, создав список перегрузок. Этот список компилятор будет использовать для проверок при вызове функции. Создадим список перегрузок, который описывает, что принимает функция pickCard и что она возвращает.
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Работаем с объектом/массивом?
// Значит, нам передали колоду и нужно выбрать карту
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Иначе даем возможность выбрать карту
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Классы
Простейший пример работы с классами:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
Наследование
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
Модификаторы доступа
В TypeScript каждый член класса будет public по умолчанию. Но можно задать и явно:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
// либо же вариант используя свойства параметров
class Animal {
constructor(public name: string) {}
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
// эти примеры абсолютно идентичны, но вторая запись короче и нагляднее
private
Когда член класса помечен модификатором private, он не может быть доступен вне этого класса:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // ошибка: 'name' is private;
// либо же вариант используя свойства параметров
class Animal {
constructor(private name: string) {}
}
// эти примеры абсолютно идентичны, но вторая запись короче и нагляднее
protected
Модификатор protected действует аналогично private за исключением того, что члены, объявленные protected, могут быть доступны в подклассах:
class Person {
constructor(protected name: string) { }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // ошибка
readonly
Вы можете делать свойства доступными только для чтения с помощью ключевого слова readonly. Свойства, доступные только для чтения, должны быть инициализированы при их объявлении или в конструкторе:
class Octopus {
readonly numberOfLegs: number = 8;
constructor (readonly name: string) {}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // ошибка! name is readonly.
Статические свойства
Вы можете делать свойства доступными только для чтения с помощью ключевого слова readonly. Свойства, доступные только для чтения, должны быть инициализированы при их объявлении или в конструкторе:
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
Абстрактные классы
Абстрактные классы — это базовые классы, от которых наследуются другие. Их экземпляры не могут быть созданы напрямую. В отличие от интерфейса, абстрактный класс может содержать детали реализации своих членов.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
Ключевое слово abstract используется для определения абстрактных классов, а также абстрактных методов в рамках таких классов.
Generics
(обобщения или шаблоны или дженерики )
позволяют создавать компоненты, способные работать с различными типами, а не только с каким-то одним. Это позволяет применять такие компоненты и использовать свои типы.
function identity<T>(arg: T): T {
return arg;
}
// компилятор автоматически устанавливает T на основании типа аргумента,
// который передается в функцию:
const A = identity('string');
const B = identity(1);
// либо задать тип явно
const С = identity<string>('hello world');
Обобщенные классы
Имеют такой же вид, что и обобщенные интерфейсы. У них есть список типовых параметров в угловых скобках (<>) после имени класса.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
Полезные ссылки для дальнейшего изучения:
- официальная документация - https://www.typescriptlang.org/
- перевод официальной документации на русский - http://typescript-lang.ru/docs/index.html
- Create React app + TypeScript - https://create-react-app.dev/docs/adding-typescript/
- Webpack + TypeScript - https://webpack.js.org/guides/typescript/
TypeScript
By Daniel Suleiman
TypeScript
- 565