Функции

Тип функции

Функция в TypeScript объявляется почти также, как и в JavaScript. Главное отличие - нужно обязательно типизировать параметры. Они типизируются при помощи :, по аналогии с предыдущими конструкциями.

function foo(a: string, b: string) {
}

Параметры функции могут быть любыми типами. При вызове функции TypeScript будет проверять совместимость типа значения, переданного в качестве параметра, с типом этого параметра. Если типы будут не совместимы - будет ошибка компиляции

foo("a", "b");

foo(10, "b"); //Error: Argument of type '10' is not assignable to parameter of type 'string'.

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

Если при вызове функции будет передано меньшее или большее количество параметров, чем было указано при объявлении - TypeScript выдаст ошибку.

function foo(a: number) {
}

foo(); //Error: Expected 1 arguments, but got 0.
foo(10, 15); //Error: Expected 1 arguments, but got 2.

Иногда возникает необходимость объявить функцию, у которой есть необязательные параметры. Для этого (как и для необязательных полей объекта) необходимо использовать символ ?.

function foo(a: number, b?: number) {
}

foo(10);
foo(10, 15);

В случае, когда параметр отмечен, как не обязательный, TypeScript объединяет тип параметра с типом undefined. Это объясняется тем, что в runtime, если необязательный параметр не был передан, то в функции его значение будет undefined. Однако иногда требуется задать какому-то параметру значение по умолчанию.

function foo(a: number = 10) {
}

foo(); //a = 10
foo(15); //a = 15

Если параметру задается значение по умолчанию, то TypeScript может применить автовывод типа параметра по значению (по обычным правилам автовывода типа).

function foo(a = 10){
}

foo() //a = 10
foo(15) //a = 15
foo("lalaka") //Error: Argument of type '"lalaka"' is not assignable to parameter of type 'number'

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

function foo(a?: number, b: string) { // Error: A required parameter cannot follow an optional parameter.
}

function bar(a = 10, b: string) {
}

function baz(a?: number, b = true) {
}

rest параметр

Чтобы передать в функцию неограниченное количество параметров, в JavaScript используется rest параметр. Для того, чтобы объявить rest параметр в TypeScript нужно указать его тип в виде массива. Если нужно передать в функцию неограниченное количество массивов, то нужно указать тип массив массивов.

function foo(...rest: number[]) {
}

rest параметр должен быть самым последним параметром (даже после опциональных). Соответственно, у функции не может быть два и более rest параметров, так как хотя бы один из них будет не последним.

Типизация this

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

function foo(this: number, b: number) {
    const c: number = this + b;
}

foo(10);

Если при определении функции был указан тип для this, то вызов методов apply и call могут принимать значение контекста, которое совместимо с типом, указанном при объявлении.

function foo(this: number) {
}

foo.call(10);
foo.call("lalaka"); //Error: Argument of type '"lalaka"' is not assignable to parameter of type 'number'.

foo.apply(15);
foo.apply(true); //Error: Argument of type 'true' is not assignable to parameter of type 'number'.

Типизация возвращаемого значения

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

function foo(): number {
}

Если тип значения, которое возвращает функция, не совместим с типом возвращаемого значения - TypeScript возвращает ошибку.

function foo(): number {
    return "lalaka"; //Error: Type '"lalaka"' is not assignable to type 'number'
}

Для того, чтобы указать, что функция ничего не возвращает, используется тип void (не never).

function foo(): void {
}

function bar(): void {
    return 10; //Error: Type '10' is not assignable to type 'void'
}

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

function foo() {
} //: void

function bar() {
    return 10;
} //: number

Вынесение типа функции

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

type F = (a: string, b?: boolean) => void;

Для описания типа важно знать только список параметров функции, их типы и тип возвращаемого значения. Тип возвращаемого значения указывается при помощи => (важно - : не используется). Проверки соответствия функции с требуемым типом осуществляется по типам параметров (включая опциональные параметры, rest параметры и типизацию this) и по типу возвращаемого значения.

function foo (a: string) {
    return 10;
}

function bar(a: number) {
    return 10;
}

function baz(a: string, b: boolean) {
    return 10;
}

function meow(...rest: string[]) {
    return 10;
}

function woof(a: string) {
}

const f1: (a: string) => number = foo;
const f2: (a: string) => number = bar; /* Error:
Type '(a: number) => void' is not assignable to type '(a: string) => void'.
  Types of parameters 'a' and 'a' are incompatible.
    Type 'string' is not assignable to type 'number'.
*/
const f3: (a: string) => number = baz; /* Error:
Type '(a: string, b: boolean) => void' is not assignable to type '(a: string) => void'.
*/
const f4: (a: string) => number = meow;
const f5: (a: string) => number = woof; /* Error:
Type '(a: string) => void' is not assignable to type '(a: string) => number'.
  Type 'void' is not assignable to type 'number'.
*/

Тип функции часто используется для определения callback-ов. Например, для типизации поля click у DOM-элемента или типизации параметра функции addEventListener. Тип функции также часто используется для определения Higher Order Function (функции, которая возвращает функцию).

interface Clickable {
    click?: (e: Event) => void;
    addEventListener: (event: "click", action: (e: Event) => void) => void;
}

const div: Clickable = {
    addEventListener: (event, action) => { /*...*/ }
}

div.click = e => console.log(e)
div.addEventListener("click", e => console.log(e));


function hof(...params: any[]): (...params: any) => boolean {
    return params.length > 0 ? (params => true) : (() => false);
}

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

type F = (a: {b: string}) => boolean;

const f1: F = (a: { b: string }) => a.b === "lalaka";
const f2: F = a => a.b === "lalaka";

Функция может содержать не все параметры, которые указаны в типе функции:

type F = (a: number) => void
const f: F = () => { };

Это возможно потому, что несмотря на то, что реализация функции использует не все (а может и никакие) параметры, указанные в типе функции, вызвать функцию все равно можно только со всеми параметрами.

f(); //Error: Expected 1 arguments, but got 0.
f(10);

Такая особенность позволяет не использовать все требуемые параметры, когда они не нужны. Это часто используется при описании callback-ов.

interface MyArray {
    [x: number]: number;
    map: (item: number, index: number, array: MyArray) => number;
}

const myArray: MyArray = {
    [0]: 0,
    [1]: 1,
    map: x => x + 1,
}

interface MyDiv {
    click: (event: Event) => void;
}

const div: MyDiv = {
    click: () => console.log("click")
}

Перегрузка

В JavaScript можно объявить функцию, которая может возвращать значения разных типов. Более того, один и тот же параметр может принимать значения разных типов. Например, опишем функцию lalaka, которая принимает 2 параметра: параметр a: "string" | "number", который указывает тип для значения второго параметра, и второй параметр b: string | number в который передается значение того типа, который указан в первом параметра. Если первый параметр "string", то второй параметр должен быть строкой, а возвращаемое значение - объект вида {a: b}. А если первый параметр "number", то второй параметр - число и в результате функция должна вернуть результат операции b + 10. Для описания такой функции в TypeScript можно попробовать воспользоваться объединением типов или типом any.

function lalaka(a: "string" | "number", b: string | number): any {
    if (a === "string") {
        return {
            a: b
        }
    } else {
        return b + 15; //Error: Operator '+' cannot be applied to types 'string | number' and 'number'.
    }
}

lalaka("number", "lalaka")
lalaka("string", 150).c === "lalaka"

В данном случаи TypeScript не может гарантировать, что если в пером параметра передано значение "number", то второй параметр будет number, то есть оба параметра считаются независимыми. Поэтому в объявлении функции ошибка. Более того, возвращаемое значение функции any, из-за чего с результатом функции можно делать что угодно (например, сравнивать значение несуществующего поля).

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

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

function foo(a: number): number;
function foo(a: string): string;
function foo(a: number | string) {
    return a;
}

В данном случаи первая и вторая сигнатура функции - это доступные для использования перегрузки (объявляются без тела функции), а последняя - это функция реализация. В функции-реализации описывается то, что на самом деле будет выполняться в runtime. Как было сказано выше, компилятор будет выбирать перегрузку подходящую под переданные параметры. Соответственно, если в функцию foo будет передано значение типа number, то компилятор выберет первую перегрузку с возвращаемым значением типа number

const a: number = foo(10) + 15;
const b: string = foo(10); // Error: Type 'number' is not assignable to type 'string'.
const c: string = foo("lalaka");

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

function foo(a: number): boolean; //Error: This overload signature is not compatible with its implementation signature.
function foo(a: string): boolean {
    return true;
}

function bar(a: number): boolean; //Error: This overload signature is not compatible with its implementation signature.
function bar(a: number): string {
    return "lalaka";
}

С использованием перегрузок функции пример функции lalaka будет выглядеть так:

function lalaka(a: "number", b: number): number
function lalaka(a: "string", b: string): {b: string};
function lalaka(a: "string" | "number", b: any) {
    if (a === "string") {
        return {
            a: b
        }
    } else {
        return b + 15;
    }
}

lalaka("number", "lalaka") //Error: No overload matches this call.
lalaka("string", 150).c === "lalaka" //Error: No overload matches this call.

const a: number = lalaka("number", 10);
const b: string = lalaka("string", "lalaka").b

Функция как объект

Ранее было сказано, что при помощи type и interface можно описать тип объекта. Но функция тоже является объектом. Можно ли ее описать при помощи этих конструкций? Да, можно.

interface B {
    (a: number, b: string): boolean
}

const b: B = (a: number, b: string) => a.toString() === b;

Интерфейс объекта, который может быть вызван, называется callable. Для того, чтобы указать сигнатуру вызова объекта, нужно в () указать параметры и их типы, а через : (не через =>) указать возвращаемое значение. В callable можно указать больше чем одну сигнатуру вызова, таким образом получить описание перегрузок.

interface A {
    (a: number): number;
    (b: string): string;
}

const foo: A = (a: any) => a;

const a: number = foo(10);
const b: string = foo("lalaka");

При помощи callable можно описать тип функции, которая не просто может быть вызвана, но и иметь собственные поля (отличающиеся от стандартных полей функции, например, length). Однако, значение, которое будет подходить для такого типа, достаточно сложно (с точки зрения типизации) собирать. Один из способов получения значения для такого типа - использовать IIFE.

interface A {
    (a: number): number;
    readonly lalaka: string;
}

const foo: A = (() => {
    const f = (a: number) => a;
    f.lalaka = "lalaka";
    return f;
})();

const a: number = foo(10);
const b: string = foo.lalaka;

Задания

Last updated