Table of Contents #

Введение #

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

Функции #

Точно так же, как и в JavaScript, функции в TypeScript могут создаваться и как именованные, и как анонимные. Это дает возможность выбрать подход, который лучше подходит конкретному приложению: создается ли группа функций для API, либо функция, которая нужна лишь для того, чтобы быть аргументом для другой функции.

Напомним, как эти два варианта выглядят в JavaScript:

// Именованная функция
function add(x, y) {
    return x + y;
}

// Анонимная функция
let myAdd = function(x, y) { return x+y; };

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

let z = 100;

function addToZ(x, y) {
    return x + y + z;
}




Типы функций #

Добавление типов к функции

Добавим к функции из предыдущих простых примеров типы:

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

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

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

Пишем тип функции

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

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

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

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

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

Вторая часть типа функции — тип возвращаемого значения. На него указывает толстая стрелка (=>) между параметрами и типом возвращаемого значения. Как писалось выше, эта часть необходима, и поэтому, если функция не возвращает ничего, в качестве типа возвращаемого значения нужно указать void.

Отметим, что параметров и типа возвращаемого значения вполне достаточно, чтобы описать тип функции. Переменные, которые функция захватывает, в ее типе не отражаются. По сути, захваченные переменные — часть так называемого "скрытого состояния" функции, и они не являются частью её API.

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

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

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

// У параметров 'x' и 'y' — тип "number"
let myAdd: (baseValue:number, increment:number) => number =
    function(x, y) { return x + y; };

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

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

В 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 с параметрами любых других типов приведет к ошибке.

Источник






Поддержите перевод документации:



Поддерживатель | Github Репозиторий


Documentation generated by mdoc.
Молния! Обновления, новости и статьи Typescript.