Spec-Zone.ru › TypeScript 5.1

Подробнее о функциях

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

Типы выражений функций

Самый простой способ описать функцию — это выражение типа функции. Эти типы синтаксически похожи на стрелочные функции:

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}
 
function printToConsole(s: string) {
  console.log(s);
}
 
greeter(printToConsole);

Синтаксис (a: string) => void означает «функция с одним параметром, имеющим имя a, типа string, которая не имеет значения возврата». Как и в случае с объявлениями функций, если тип параметра не указан, он неявно any.

Обратите внимание, что имя параметра обязательно. Тип функции (string) => void означает «функция с параметром с именем string типа any»!

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

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

Подписи вызова

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

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}
 
function myFunc(someArg: number) {
  return someArg > 3;
}
myFunc.description = "default description";
 
doSomething(myFunc);

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

Подписи конструктора

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

type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Некоторые объекты, такие как объект JavaScript Date , могут вызываться с или без new. Вы можете произвольно комбинировать подписи вызова и конструктора в одном типе:

interface CallOrConstruct {
  new (s: string): Date;
  (n?: number): string;
}

Обобщенные функции

Часто пишут функцию, где типы входных данных связаны с типом выходных данных или где типы двух входных данных каким-то образом связаны. Давайте на мгновение рассмотрим функцию, возвращающую первый элемент массива:

function firstElement(arr: any[]) {
  return arr[0];
}

Эта функция выполняет свою задачу, но, к сожалению, имеет тип возвращаемого значения any. Было бы лучше, если функция возвращала тип элемента массива.

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

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

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

// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);

Вывод

Обратите внимание, что нам не пришлось указывать Type в этом примере. Тип был выведен — автоматически выбран — TypeScript.

Мы также можем использовать несколько параметров типа. Например, автономная версия map будет выглядеть так:

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
 
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

Обратите внимание, что в этом примере TypeScript смог вывести как тип параметра Input (из заданного массива string), так и параметр типа Output на основе значения возврата выражения функции (number).

Ограничения

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

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

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}
 
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);

В этом примере несколько интересных моментов. Мы позволили TypeScript вывести тип возвращаемого значения longest. Вывод типов также работает с обобщенными функциями.

Поскольку мы ограничили Type до { length: number }, мы могли получить доступ к свойству .length параметров a и b . Без ограничений типа мы не смогли бы получить доступ к этим свойствам, потому что значения могли бы быть другого типа без свойства length.

Типы longerArray и longerString были выведены на основе аргументов. Помните, обобщения предназначены для связывания двух и более значений с одинаковым типом!

Наконец, как мы и хотели, вызов longest(10, 100) отклоняется, потому что тип number не имеет свойства .length.

Работа со значениями, ограниченными условиями

Вот распространённая ошибка при работе с обобщёнными ограничениями:

function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj;
  } else {
    return { length: minimum };
  }
}

Может показаться, что эта функция в порядке — Type ограничен { length: number }, и функция возвращает Type или значение, соответствующее этому ограничению. Проблема в том, что функция обещает вернуть одинаковый вид объекта, что и был передан, а не просто любой объект, соответствующий ограничению. Если бы этот код был корректным, можно было бы написать код, который определённо не сработал бы:

// 'arr' gets value { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));

Указание аргументов типа

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

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

Обычно было бы ошибкой вызвать эту функцию с несовпадающими массивами:

const arr = combine([1, 2, 3], ["hello"]);

Однако, если вы намеревались сделать это, вы можете вручную указать Type:

const arr = combine<string | number>([1, 2, 3], ["hello"]);

Рекомендации по написанию хороших обобщённых функций

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

Опускание параметров типа

Вот два способа написания функции, которые выглядят похожими:

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}
 
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}
 
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

На первый взгляд они могут показаться идентичными, но firstElement1 — намного лучший способ написать эту функцию. Её выводимый тип возврата — Type, но для firstElement2 выводимый тип возврата — any, потому что TypeScript должен разрешить выражение arr[0] с использованием типа ограничения, а не «ждать» разрешения элемента во время вызова.

Правило: Если возможно, используйте сам параметр типа, а не его ограничение

Использование меньшего количества параметров типа

Вот ещё одна пара похожих функций:

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}
 
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

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

Правило: Всегда используйте как можно меньше параметров типа

Параметры типа должны появляться дважды

Иногда мы забываем, что функция может не нуждаться в обобщении:

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
 
greet("world");

Мы могли бы написать более простую версию:

function greet(s: string) {
  console.log("Hello, " + s);
}

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

Правило: Если параметр типа появляется только в одном месте, серьёзно подумайте, действительно ли вам нужен этот параметр типа

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

Функции в JavaScript часто принимают переменное количество аргументов. Например, метод toFixed объекта number принимает необязательное число цифр:

function f(n: number) {
  console.log(n.toFixed()); // 0 arguments
  console.log(n.toFixed(3)); // 1 argument
}

Мы можем смоделировать это в TypeScript, пометив параметр как необязательный с ?:

function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK

Хотя параметр указан как тип number, параметр x фактически будет иметь тип number | undefined, потому что неуказанные параметры в JavaScript получают значение undefined.

Вы также можете задать параметру значение по умолчанию:

function f(x = 10) {
  // ...
}

Теперь в теле f, x будет иметь тип number, потому что любой аргумент undefined будет заменён на 10. Обратите внимание, что когда параметр является необязательным, вызывающие функции всегда могут передавать undefined, так как это просто имитирует «отсутствующий» аргумент:

declare function f(x?: number): void;
// cut
// All OK
f();
f(10);
f(undefined);

Необязательные параметры в обратных вызовах

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

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

То, что люди обычно подразумевают при написании index? как необязательного параметра, заключается в том, что они хотят, чтобы оба эти вызова были допустимы:

myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

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

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    // I don't feel like providing the index today
    callback(arr[i]);
  }
}

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

myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
});

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

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

Перегрузки функций

Некоторые функции JavaScript могут быть вызваны с различным количеством и типами аргументов. Например, вы можете написать функцию для создания Date , которая принимает либо временную метку (один аргумент), либо указание месяца/дня/года (три аргумента).

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

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);

В этом примере мы написали две перегрузки: одну, принимающую один аргумент, и другую, принимающую три аргумента. Эти первые две сигнатуры называются перегруженными сигнатурами.

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

Перегруженные сигнатуры и реализующая сигнатура

Это распространённый источник путаницы. Часто люди пишут код подобного вида и не понимают, почему возникает ошибка:

function fn(x: string): void;
function fn() {
  // ...
}
// Expected to be able to call with zero arguments
fn();

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

Сигнатура реализации не видна извне. При написании перегруженной функции всегда должно быть две или более сигнатуры над реализацией функции.

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

function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
function fn(x: boolean) {}
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
function fn(x: string | number) {
  return "oops";
}

Написание хороших перегрузок

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

Рассмотрим функцию, возвращающую длину строки или массива:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
}

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

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);

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

function len(x: any[] | string) {
  return x.length;
}

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

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

Объявление this в функции

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

const user = {
  id: 123,
 
  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

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

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

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

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(() => this.admin);

Другие типы, которые следует знать

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

void

void представляет возвращаемое значение функций, которые не возвращают значение. Это тип, который определяется автоматически, когда функция не содержит инструкций return или не возвращает явное значение из этих инструкций возврата:

// The inferred return type is void
function noop() {
  return;
}

В JavaScript функция, которая не возвращает значения, неявно возвращает значение undefined. Однако, void и undefined — это не одно и то же в TypeScript. Более подробная информация приведена в конце этой главы.

void не то же самое, что undefined.

object

Специальный тип object относится к любому значению, которое не является примитивным (string, number, bigint, boolean, symbol, null, или undefined). Это отличается от пустого типа объекта { }, а также от глобального типа Object. Вероятность того, что вам придётся использовать Object , очень мала.

object не является Object. Всегда используйте object!

Обратите внимание, что в JavaScript значения функций являются объектами: у них есть свойства, у них есть Object.prototype в их цепочке прототипов, они являются instanceof Object, вы можете вызвать Object.keys на них и так далее. По этой причине типы функций считаются object в TypeScript.

unknown

Тип unknown представляет любое значение. Это похоже на тип any , но безопаснее, поскольку делать что-либо со значением unknown не допускается:

function f1(a: any) {
  a.b(); // OK
}
function f2(a: unknown) {
  a.b();
}

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

И наоборот, вы можете описать функцию, возвращающую значение неизвестного типа:

function safeParse(s: string): unknown {
  return JSON.parse(s);
}
 
// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);

never

Некоторые функции никогда не возвращают значение:

function fail(msg: string): never {
  throw new Error(msg);
}

Тип never представляет значения, которые никогда не наблюдаются. В типе возвращаемого значения это означает, что функция выбрасывает исключение или прерывает выполнение программы.

never также появляется, когда TypeScript определяет, что в объединении ничего не осталось.

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // has type 'never'!
  }
}

Function

Глобальный тип Function описывает свойства, такие как bind, call, apply и другие, присутствующие во всех значениях функций в JavaScript. Он также имеет специальное свойство, что значения типа Function всегда могут быть вызваны; эти вызовы возвращают any:

function doSomething(f: Function) {
  return f(1, 2, 3);
}

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

Если вам нужно принять произвольную функцию, но вы не хотите её вызывать, тип () => void обычно безопаснее.

Остаточные параметры и аргументы

Дополнительное чтение:
Остаточные параметры
Синтаксис распространения

Остаточные параметры

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

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

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

В TypeScript аннотация типа для этих параметров неявно является any[] вместо any, и любая заданная аннотация типа должна иметь вид Array<T> или T[], или тип кортежа (о котором мы узнаем позже).

Остаточные аргументы

И наоборот, мы можем предоставить переменное количество аргументов из итерируемого объекта (например, массива) с помощью синтаксиса распространения. Например, метод push массивов принимает любое количество аргументов:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

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

// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);

Лучшее решение для этой ситуации зависит от вашего кода, но в общем случае контекст const является наиболее простым решением:

// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);

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

Деструктуризация параметров

Дополнительное чтение:
Деструктурирующее присваивание

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

function sum({ a, b, c }) {
  console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });

Аннотация типа для объекта идёт после синтаксиса деструктуризации:

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}

Это может выглядеть немного громоздко, но вы также можете использовать здесь именованный тип:

// Same as prior example
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}

Совместимость функций

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

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

Контекстная типизация с типом возврата void не принуждает функции не возвращать что-либо. Другой способ сказать это — контекстная функция типа с типом возврата void (type voidFunc = () => void ), когда она реализована, может вернуть любое другое значение, но оно будет проигнорировано.

Таким образом, следующие реализации типа () => void являются допустимыми:

type voidFunc = () => void;
 
const f1: voidFunc = () => {
  return true;
};
 
const f2: voidFunc = () => true;
 
const f3: voidFunc = function () {
  return true;
};

И когда возвращаемое значение одной из этих функций присваивается другой переменной, оно сохранит тип void:

const v1 = f1();
 
const v2 = f2();
 
const v3 = f3();

Это поведение существует для того, чтобы следующий код был валидным, даже если Array.prototype.push возвращает число, а метод Array.prototype.forEach ожидает функцию с возвращаемым типом void.

const src = [1, 2, 3];
const dst = [0];
 
src.forEach((el) => dst.push(el));

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

function f2(): void {
  // @ts-expect-error
  return true;
}
 
const f3 = function (): void {
  // @ts-expect-error
  return true;
};

Для получения дополнительной информации о void обратитесь к этим документам:

  • Руководство v1
  • Руководство v2
  • FAQ — «Почему функции, возвращающие не void, могут быть присвоены функциям, возвращающим void?»

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/2/functions.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API