8. Примеси

Примеси

Для построения иерархии объектов помимо традиционного объектно-ориентированного подхода существует другой популярный способ создания классов из переиспользуемых компонентов - это создание класса путем объединения более простых частичных классов. Возможно, вам знакома идея примесей по таким языкам как Scala. Такой подход так же стал популярен и в JavaScript.

Пример примеси

В примере ниже показано, как можно смоделировать примеси в TypeScript. После кода будет рассказано как он работает.

// Одноразовая примесь
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }
 
}
 
// Активируемая примесь
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}
 
class SmartObject implements Disposable, Activatable {
    constructor() {
        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
    }
 
    interact() {
        this.activate();
    }
 
    // Одноразовый
    isDisposed: boolean = false;
    dispose: () => void;
    // Активируемый
    isActive: boolean = false;
    activate: () => void;
    deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable])
 
var smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
 
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        })
    }); 
}

Как понимать этот пример

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

// Одноразовая примесь
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }
 
}
 
// Активируемая примесь
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

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

class SmartObject implements Disposable, Activatable {

Для начала обратите внимание, что вместо 'extends' используется ключевое слово 'implements'. Это позволяет представить классы в качестве интерфейсов, и использовать только типы Disposable и Activatable, но не реализацию классов. Тем не менее реализацию частично нужно будет описать в новом классе. 

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

// Одноразовая примесь
isDisposed: boolean = false;
dispose: () => void;
// Активируемая примесьisActive: boolean = false;
activate: () => void;
deactivate: () => void;

И далее примеси объединяются в класс:

applyMixins(SmartObject, [Disposable, Activatable])

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

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        })
    }); 
}

7. Типичные ошибки

Типичные ошибки


Ниже представлен список наиболее часто встречающихся сообщений об ошибках, который вы можете встретить при работе с компилятором TypeScript

"tsc.exe" exited with error code 1.

Fixes:

external module XYZ cannot be resolved

Fixes:

6. Обобщения

Обобщения

Одна из ключевых идей в разработке ПО - это создание многократно используемых модулей.
В таких языках как C# или Java для разработки многократно используемых модулей используется такой инструмент как 'generics', позволяющий создавать компоненты, которые могут работать со множеством типов, а не с одним. И это позволяет использовать в этих компонентах типы собственной разработки.

"Hello World" с помощью обобщений

Для начала создадим "hello world" с помощью обощенных типов: функцию идентификации, которая будет возвращать все, что в нее передано. Что то вроде команды 'echo'.

Без обобщенных типов необходимо явно указывать тип аргумента и тип возвращаемого значения:

function identity(arg: number): number {
    return arg;
}

Но можно в качестве типа указать тип 'any':

function identity(arg: any): any {
    return arg;
}

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

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

function identity<T>(arg: T): T {
    return arg;
}

Здесь добавлена переменная типа 'T'. Она позволяет зафиксировать тип аргумента для последующего использования. Так же 'T' указывается как возвращаемый тип. Таким образом получается, что возвращается именно такой тип, который передан в функцию.

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

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


var output = identity<string>("myString");  // результат будет типа 'string'

Здесь явно указыватся, что 'T' будет строка.

Второй способ, возможно, более частый - это не указывать 'T', при этом компилятор автоматически установит значение 'T' в зависимости от типа аргумента:

var output = identity("myString");  // результат будет типа 'string'

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

Работа с переменными обобщений

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


Посмотрим на первоначальную функцию:
 
function identity<T>(arg: T): T {
    return arg;
}

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

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Ошибка: T не имеет .length
    return arg;
}

В этом случае компилятор выдаст ошибку, что используется поле ".length" аргумента 'arg', но нигде не указано, что 'arg' имеет это поле. Не все типы имеют это поле.

Перепишем функцию так, чтобы работать с массивом переменных типа T. Тогда у аргумента будет поле .length:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Ошибки нет, т.к. у массива имеется поле .length
    return arg;
}

Можно сказать, что обощенная функция loggingIdentity принимает параметр типа T и аргумент 'arg' в виде массива T, и возвращает массив T. Если в функцию передать массив чисел, функция вернет массив чисел.

Эту функцию можно переписать следующим образом:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Ошибки нет, т.к. у массива имеется поле .length
    return arg;
}

Возможно вы уже знакомы с таким стилем из других языков. В следующей секции будет рассказано, как можно создавать собственнве обобщенные типы, такие как Array<T>.

Обобщенные типы

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

Обощенный функции похожи на необобщенные, но вначале указывается тип параметра, как в декларации функции:

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: <T>(arg: T)=>T = identity;

Но можно указывать и другое имя обобщающего типа.

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: <U>(arg: U)=>U = identity;

Или так:

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: {<T>(arg: T): T} = identity;

Напишем первый обощенный интерфейс. Изменим предыдущий пример с использованием интерфейса:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: GenericIdentityFn = identity;

Обобщенный параметр можно вынести в интерфейс. В этом случае тип параметра будет виден всем членам интерфейса.

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: GenericIdentityFn<number> = identity;

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

Обобщенные классы

Обобщенные классы выглядят так же как обобщенные интерфейсы.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

var myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

Это очень похоже на класс 'GenericNumber'.

var stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

Теперь можно гарантировать, что все свойства класса работают с одним и тем же типом.

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

Ограничения обобщений

Если вы понимте предыдущий пример, иногда нужно написать обобщенную функцию с несколькими параметрами, где известны некоторые особенности этих типов параметров. В примере 'loggingIdentity', мы хотим получать доступ к ".length" свойству переменной 'arg', но компилятор не знает, что каждый тип имеет свойство ".length", поэтому он сообщает об ошибке.

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Ошибка: T не имеет .length
    return arg;
}

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

Для этого надо создать интерфейс, который будет описывать наше ограничение. Создадим интерфейс с одним свойством ".length", а затем используем его:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Теперь мы знаем, что свойство .length имеется
    return arg;
}

Но теперь функция не будет работать с абсолютно всеми типами:

loggingIdentity(3);  // Ошибка, number не имеет свойство .length 

Вместо этого необходимо передавать параметры с явным указанием свойств:

loggingIdentity({length: 10, value: 3});  

Использование параметров типов в ограничениях обобщений

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

function find<T, U extends Findable<T>>(n: T, s: U) {   // ошибка
  // ...
} 
find (giraffe, myAnimals);

Решить эту задачу можно заменив параметр типа ограничением:

function find<T>(n: T, s: Findable<T>) {   
  // ...
} 
find(giraffe, myAnimals);


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

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

function create<T>(c: {new(): T; }): T { 
    return new c();
}

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

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string; 
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function findKeeper<A extends Animal, K> (a: {new(): A; 
    prototype: {keeper: K}}): K {

    return a.prototype.keeper;
}

findKeeper(Lion).nametag;  // typechecks!

5. Функции

Функции

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

Функции

Так же как и в JavaScript, в TypeScript функции могут быть как именованными так и анонимными:
//Именованная функция
function add(x, y) {
    return x+y;
}

//Анонимная функция
var myAdd = function(x, y) { return x+y; };
Так же как и в JavaScript функции могут возвращать переменные. 


Типы возвращаемых значений

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

function add(x: number, y: number): number {
    return x+y;
}

var myAdd = function(x: number, y: number): number { return x+y; };


Написание типа функции

Функция представляет из себя тип. Рассмотрим полное написание типа функции. 

var myAdd: (x:number, y:number)=>number = 
    function(x: number, y: number): number { return x+y; };

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

var myAdd: (baseValue:number, increment:number)=>number = 
    function(x: number, y: number): number { return x+y; };


Вторая часть - это возвращаемое значение. Тип возвращаемого значения указывается после стрелки =>. В описании типа функции указывать тип возвращаемого значения обязательно, поэтому, ксли функция не возвращает значение, необходимо указать 'void', но не оставлять тип возвращаемого значения пустым.
Важно иметь в виду, что только параметры и тип возвращаемого значения формируют тип функции.

Выведение типов

Компилятор TypeScript может сам определять тип выражения, основываясь на типах аргументов:

// myAdd имеет полное описание типа функции
var myAdd = function(x: number, y: number): number { return x+y; };

// Параметры 'x' и 'y' имеют тип number
var myAdd: (baseValue:number, increment:number)=>number = 
    function(x, y) { return x+y; };

Это называется 'контекстная типизация'. Это позволяет сократить усилия, чтобы программа была типизированной.

Необязательные параметры и параметры по умолчанию

В отличие от JavaScript, в TypeScript каждый параметр функции обязательный. Это не означает, что он не может быть 'null', при вызове функции компилятор проверяет, что указаны все параметры. Количество передаваемых в функцию параметров должно совпадать с количеством параметров, описанных в функции.

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}

var result1 = buildName("Bob");  //ошибка, мало параметров
var result2 = buildName("Bob", "Adams", "Sr.");  //ошибка, много параметров
var result3 = buildName("Bob", "Adams");  //нет ошибки

В JavaScript каждый параметр считается необязательным, и пользователи могут не указывать их. Тогда значения этих параметров будут неопределенными ('undefined'). Такой же функциональности в TypeScript можно добиться использованием '?' рядом с параметрами, которые мы хотим сделать необязательнвми. Например, если мы хотим, чтобы последний параметр был необязательным:


function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

var result1 = buildName("Bob");  //работает
var result2 = buildName("Bob", "Adams", "Sr.");  //ошибка, много параметров
var result3 = buildName("Bob", "Adams");  //работает

Необязательные параметры должны строго следовать после обязательных.

В TypeScript можно указать значение необязательного параметра по умолчанию, если пользователь не его указал. Например, установим значение по умолчанию для последнего параметра "Smith".

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

var result1 = buildName("Bob");  //работает
var result2 = buildName("Bob", "Adams", "Sr.");  //ошибка, много параметров
var result3 = buildName("Bob", "Adams");  //работает

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


Необязательные параметры и параметры со значением по умолчанию тоже показывают какого они типа. Оба варианта:

function buildName(firstName: string, lastName?: string) {

и

function buildName(firstName: string, lastName = "Smith") {

одного и того же типа "(firstName: string, lastName?: string)=>string". Значение по умолчанию для необязательного параметра не указывается, а указывается только то, что параметр необязательный.

Однотипные параметры

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

В TypeScript аргументы можно объединить в переменную:

function buildName(firstName: string, ...restOfName: string[]) {
 return firstName + " " + restOfName.join(" ");
}

var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

Однотипные параметры рассматриваются как неограниченное число необязательных параметров. Можно не указывать ни одного или указать сколько угодно. Компилятор сформирует массив аргументов, которые указаны после многоточния (...), и этот массив будет доступен в тебе функции. 


Многоточние также используется в типе функции с однотипными параметрами:

function buildName(firstName: string, ...restOfName: string[]) {
 return firstName + " " + restOfName.join(" ");
}

var buildNameFun: (fname: string, ...rest: string[])=>string = buildName;

Ламбда и ключевое слово this

Как работает 'this' в функциях JavaScript это очень непростая тема для программистов использующих JavaScript. В действительности, понимание как работает this часто приходит только с опытом использования JavaScript. Так как TypeScript это расширение JavaScript, то разработчику TypeScript также необходимо изучить как использовать 'this' и как определить когда оно используется неправильно. Целая статья может быть написана на тему как работает 'this' В JavaScript. Рассмотрим только основы. 

В JavaScript 'this' это переменная, которая устанавливается, когда вызывается функция. Это очень полезная возможность, но она устанавливается с учетом понимания того, в каком контексте выполняется функция. Это может ввести в заблуждение, например, когда используется функция обратного вызова

Например:

var deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        return function() {
            var pickedCard = Math.floor(Math.random() * 52);
            var pickedSuit = Math.floor(pickedCard / 13);
   
            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

Здесь вместо сообщения будет показана ошибка. Потому что используется 'this' созданный в функции 'createCardPicker', который окажется установленный в 'window', а не на объект 'deck'. Это получается в результате вызова функции 'cardPicker()'. Здесь не используется динамическое связывание для 'this' как это делается для Window. (надо иметь в виду: в строгом режиме, this будет 'undefined', а не window).

Это можно исправить, если убедиться, что функция связана с правильным 'this', прежде чем вернуться в функцию, которая будет использоваться позже. В этом случае, независимо от того как она будет использоваться позже, все равно будет возможноть видеть оригинальный объект 'deck'.

Чтобы это исправить, перепишем функцию так, чтобы использовалась лямда ( ()=>{} ). Тогда 'this' будет автоматически установлен, когда функция создана, а не когда она вызывается:

var deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // Notice: the line below is now a lambda, allowing us to capture 'this' earlier
        return () => {
            var pickedCard = Math.floor(Math.random() * 52);
            var pickedSuit = Math.floor(pickedCard / 13);
   
            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

Для большего понимания работы 'this' почитайте Yahuda Katz's Understanding JavaScript Function Invocation and “this”.

Перегрузка

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

var suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        var pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        var pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
var pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

var pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

Здесь функция 'pickCard' возвращает разные вещи в зависимости от того, что в нее передано.Если в нее передан deck, она вернет card. Если в нее передан card, то указывается какой именно card передан. Но как это описывается в системе типов?

Ответ - использовать перегрузку. В зависимости от параметров компилятор выбирает какую вызывать функцию. Например:

var 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 {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        var pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        var pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
var pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

var pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

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