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

Типы функций

Добавим к функции типы:

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

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

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

Опциональные параметры и параметры по умолчанию

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

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

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

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

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");         // в самый раз

Все необязательные параметры должны идти после обязательных. Если бы первый параметр (firstName) нужно было сделать опциональным вместоlastName, то порядок параметров в функции пришлось бы изменить, чтобыfirstNameоказался последним.

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

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");         // в самый раз

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

function buildName(firstName: string, lastName?: string) {
    // ...
}

и эта

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

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

В отличие от простых опциональных параметров, параметры по умолчаниюне обязательнодолжны находиться после обязательных параметров. Если после параметра по умолчанию будет идти обязательный, то придется явно передатьundefined, чтобы задать значение по умолчанию. К примеру, последний пример можно переписать, используя дляfirstNameтолько параметр по умолчанию:

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

let result1 = buildName("Bob");                  // ошибка, слишком мало параметров
let result2 = buildName("Bob", "Adams", "Sr.");  // ошибка, слишком много параметров
let result3 = buildName("Bob", "Adams");         // подходит, возвратит "Bob Adams"
let result4 = buildName(undefined, "Adams");     // подходит, возвратит "Will Adams"

Остаточные параметры (rest parameters)

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

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

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

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

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

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

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

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

this

Научиться правильно использоватьthisв JavaScript — нечто вроде обряда посвящения в разработчики. Поскольку TypeScript — это надмножество JavaScript, программистам на TypeScript также нужно понимать, как использоватьthisи как замечать, когдаthisиспользуется неправильно. К счастью, TypeScript позволяет обнаруживать неправильное использованиеthisс помощью нескольких приемов. Если вам только предстоит разобраться с тем, как работаетthis, то для начала прочтите статью Yehuda KatzПонятие о вызове функций в JavaScript и "this". Эта статья очень хорошо объясняет, как работаетthis"под капотом", поэтому здесь мы рассмотрим только основы.

Прим. переводчика — на русском языке по данной теме можно посоветовать прочестьсоответствующую статью из учебника javascript.ru, а такжеКлючевое слово this в JavaScript.

thisи стрелочные функции

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

Давайте посмотрим на пример:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        return function() {
            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);

Обратите внимание, чтоcreateCardPicker— функция, которая возвращает функцию. Если попытаться запустить этот пример, то мы получим ошибку вместо ожидаемого сообщения. Так происходит по той причине, чтоthis, которая используется в функции, созданнойcreateCardPicker, указывает наwindow, а не на объектdeck. Все это из-за того, чтоcardPicker()вызывается сама по себе. При использовании подобного синтаксиса, когда функция вызывается не как метод, и при том на самом верхнем уровне программы,thisбудет указывать наwindow. (Замечание: в режиме соответствия стандартам (strict mode) в таких случаяхthisбудет иметь значениеundefined, а неwindow).

Можно исправить это, удостоверившись в том, что функция привязана к правильному значениюthis, прежде чем возвращать ее. В таком случае, независимо от того, как она будет использоваться в дальнейшем, ей все равно будет доступен оригинальный объектdeck. Чтобы сделать это, нужно изменить функцию, и использовать синтаксис стрелочной функции из стандарта ECMAScript 6. Стрелочные функции захватывают значениеthisтаким, каким оно было на момент ее создания (а не во время вызова):

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // ВНИМАНИЕ: строка ниже — стрелочная функция, которая захватывает значение 'this' из этого места
        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);

Что еще лучше, если передать компилятору флаг --noImplicitThis, то TypeScript будет выдавать предупреждение, если вы сделаете подобную ошибку. Он укажет на то, что thisв выражении this.suits[pickedSuit]имеет типany.

Параметрыthis

К сожалению, тип выраженияthis.suits[pickedSuit]по прежнемуany, посколькуthisберется из функционального выражения внутри объектного литерала. Чтобы исправить это, можно явно указатьthisв качестве параметра. Параметрthis— это "фальшивый" параметр, который идет первым в списке параметров функции:

function f(this: void) {
    // Гарантировать, что в этой отдельной функции 'this' использовать нельзя
}

Добавим к предыдущему примеру несколько интерфейсов:CardиDeck, чтобы сделать типы более понятными и простыми для повторного использования:

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);

Теперь компилятор знает, что функцияcreateCardPickerожидает, что будет вызвана на объекте с типомDeck. Это значит, что тип значенияthisтеперь —Deck, а неany, и флаг--noImplicitThisне будет выдавать ошибок.

Параметрыthisдля функций обратного вызова

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

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

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

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // Тут используется this! Эта функция упадет во время выполнения!
        this.info = e.message;
    };
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!

Когдаthisуказан, это явно отражает тот факт, чтоonClickBadдолжна вызываться на экземпляре классаHandler. Теперь TypeScript обнаружит, чтоaddClickListenerтребует функцию сthis: void. Чтобы исправить эту ошибку, изменим типthis:

class Handler {
    info: string;
    onClickGood(this: void, e: Event) {
        // здесь нельзя использовать переменную this, потому что у нее тип void!
        console.log('clicked!');
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

Так как в функцииonClickGoodуказано, что типthisvoid, ее можно передать вaddClickListener. Конечно, это означает и то, что теперь в ней нельзя использоватьthis.info. Но если нужно и то, и другое, то придется использовать стрелочную функцию:

class Handler {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message }
}

Это будет работать, поскольку стрелочные функции не захватываютthisиз контекста, в котором выполняются, и их можно свободно передавать там, где ожидается функция сthis: void. Недостаток такого решения в том, что для каждого объектаHandlerбудет создаваться своя стрелочная функция. Методы же, напротив, создаются только однажды, ассоциируются с прототипомHandler, и являются общими для всех объектов этого класса.

Перегрузки

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

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

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);

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

Нужно указать для одной функции несколько типов, создав список перегрузок. Этот список компилятор будет использовать для проверок при вызове функции. Создадим список перегрузок, который описывает, что принимает функция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);

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

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

Обратите внимание, что участок кодаfunction pickCard(x): anyне входит в список перегрузок; в этом списке всего два элемента, один из которых принимаетobject, а другой — число. ВызовpickCardс параметрами любых других типов приведет к ошибке.

results matching ""

    No results matching ""