函数进阶

函数是任何应用程序的基本构建块,无论是本地函数、从另一个模块导入的函数还是类上的方法。它们也是值,就像其他值一样,TypeScript 有很多方法来描述如何调用函数。让我们学习如何编写描述函数的类型。

函数类型表达式

描述函数的最简单方法是使用函数类型表达式。这些类型在语法上类似于箭头函数

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

语法 (a: string) => void 表示“一个带有一个名为 a 的参数的函数,类型为 string,没有返回值”。就像函数声明一样,如果未指定参数类型,则隐式为 any

请注意,参数名称是必需的。函数类型(string) => void表示“一个参数名为string,类型为any的函数”!

当然,我们可以使用类型别名来命名函数类型

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

调用签名

在 JavaScript 中,函数除了可调用之外,还可以具有属性。但是,函数类型表达式语法不允许声明属性。如果我们想描述具有属性的可调用对象,可以在对象类型中编写调用签名

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

请注意,语法与函数类型表达式略有不同 - 在参数列表和返回类型之间使用:,而不是=>

构造签名

JavaScript 函数也可以使用new运算符调用。TypeScript 将这些称为构造函数,因为它们通常会创建一个新对象。您可以通过在调用签名前面添加new关键字来编写构造签名

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

某些对象,例如 JavaScript 的Date对象,可以带或不带new调用。您可以任意组合同一类型中的调用和构造签名

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

泛型函数

通常编写一个函数,其中输入的类型与输出的类型相关,或者两个输入的类型以某种方式相关。让我们考虑一下返回数组第一个元素的函数

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

此函数完成了它的工作,但不幸的是返回类型为any。如果函数返回数组元素的类型会更好。

在 TypeScript 中,当我们想要描述两个值之间的对应关系时,使用泛型。我们通过在函数签名中声明类型参数来做到这一点

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

通过向此函数添加一个类型参数Type并在两个地方使用它,我们创建了函数输入(数组)和输出(返回值)之间的联系。现在,当我们调用它时,会返回一个更具体的类型。

ts
// 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([]);
Try

推断

请注意,我们不必在此示例中指定Type。类型是推断的 - 由 TypeScript 自动选择。

我们也可以使用多个类型参数。例如,map 的独立版本看起来像这样

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

请注意,在此示例中,TypeScript 可以推断出Input 类型参数的类型(来自给定的string 数组),以及基于函数表达式返回值(number)的Output 类型参数。

约束

我们编写了一些可以对任何类型的值起作用的泛型函数。有时我们想关联两个值,但只能对特定值的子集进行操作。在这种情况下,我们可以使用约束来限制类型参数可以接受的类型。

让我们编写一个返回两个值中较长的函数。为此,我们需要一个length 属性,该属性是一个数字。我们通过编写extends 子句来约束类型参数到该类型。

ts
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);
Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.2345Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
Try

在此示例中需要注意一些有趣的事情。我们允许 TypeScript 推断longest 的返回类型。返回类型推断也适用于泛型函数。

因为我们将Type 约束为{ length: number },所以我们被允许访问ab 参数的.length 属性。如果没有类型约束,我们将无法访问这些属性,因为这些值可能是没有长度属性的其他类型。

longerArraylongerString 的类型是根据参数推断出来的。请记住,泛型都是关于用相同类型关联两个或多个值!

最后,正如我们所期望的那样,对longest(10, 100) 的调用被拒绝,因为number 类型没有.length 属性。

使用受限值

以下是使用泛型约束时常见的错误

ts
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.2322Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
Try

这个函数看起来没问题 - Type 被约束为 { length: number },函数要么返回 Type,要么返回一个匹配该约束的值。问题是,该函数承诺返回与传入的相同类型的对象,而不仅仅是匹配约束的某个对象。如果这段代码是合法的,你就可以编写一些肯定无法正常工作的代码

ts
// '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));
Try

指定类型参数

TypeScript 通常可以推断出泛型调用中预期的类型参数,但并非总是如此。例如,假设你编写了一个函数来合并两个数组

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

通常,用不匹配的数组调用此函数会报错

ts
const arr = combine([1, 2, 3], ["hello"]);
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

但是,如果你想这样做,可以手动指定 Type

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

编写优质泛型函数的指南

编写泛型函数很有趣,而且很容易沉迷于类型参数。类型参数过多或在不需要的地方使用约束会导致推断效率降低,从而让调用你的函数的人感到沮丧。

将类型参数下移

以下是两种看起来相似的函数编写方式

ts
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]);
Try

乍一看,它们可能看起来完全相同,但firstElement1是编写此函数的更好方法。它的推断返回类型是Type,而firstElement2的推断返回类型是any,因为TypeScript必须使用约束类型来解析arr[0]表达式,而不是“等待”在调用期间解析元素。

规则:尽可能使用类型参数本身,而不是对其进行约束

使用更少的类型参数

以下是另一对类似的函数

ts
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);
}
Try

我们创建了一个类型参数Func,它不关联两个值。这始终是一个危险信号,因为它意味着想要指定类型参数的调用者必须手动指定一个额外的类型参数,而没有理由。Func除了使函数更难阅读和理解之外,什么也不做!

规则:始终使用尽可能少的类型参数

类型参数应该出现两次

有时我们会忘记一个函数可能不需要是泛型的

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

我们可以轻松地编写一个更简单的版本

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

请记住,类型参数用于关联多个值的类型。如果类型参数在函数签名中只使用一次,它就没有关联任何东西。这包括推断的返回类型;例如,如果Strgreet的推断返回类型的一部分,它将关联参数和返回类型,因此将被使用两次,尽管在书面代码中只出现一次。

规则:如果类型参数只在一个地方出现,请认真考虑是否真的需要它。

可选参数

JavaScript 中的函数通常接受可变数量的参数。例如,numbertoFixed 方法接受一个可选的数字位数。

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

我们可以在 TypeScript 中通过使用 ? 将参数标记为可选来模拟这种行为。

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

虽然参数被指定为 number 类型,但 x 参数实际上将具有 number | undefined 类型,因为 JavaScript 中未指定的参数将获得 undefined 值。

您也可以提供一个参数默认值

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

现在在 f 的主体中,x 将具有 number 类型,因为任何 undefined 参数都将被替换为 10。请注意,当参数是可选时,调用者始终可以传递 undefined,因为这只是模拟了“缺失”参数。

ts
// All OK
f();
f(10);
f(undefined);
Try

回调中的可选参数

一旦您了解了可选参数和函数类型表达式,在编写调用回调的函数时,很容易犯以下错误。

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

人们在将 index? 编写为可选参数时通常的意图是,他们希望这两个调用都是合法的。

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

实际上意味着callback 可能会用一个参数调用。换句话说,函数定义表示实现可能看起来像这样。

ts
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]);
}
}
Try

反过来,TypeScript 将强制执行此含义并发出实际上不可能发生的错误。

ts
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed());
'i' is possibly 'undefined'.18048'i' is possibly 'undefined'.
});
Try

在 JavaScript 中,如果您使用比参数更多的参数调用函数,则额外的参数将被简单地忽略。TypeScript 的行为方式相同。参数更少(类型相同)的函数始终可以代替参数更多的函数。

规则:在为回调编写函数类型时,永远不要编写可选参数,除非您打算调用该函数而不传递该参数。

函数重载

一些 JavaScript 函数可以用多种参数数量和类型调用。例如,您可能编写一个函数来生成一个 `Date`,该函数接受一个时间戳(一个参数)或一个月份/日期/年份规范(三个参数)。

在 TypeScript 中,我们可以通过编写重载签名来指定一个可以以不同方式调用的函数。为此,编写一些函数签名(通常两个或更多),然后是函数体。

ts
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);
No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.2575No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
Try

在这个例子中,我们编写了两个重载:一个接受一个参数,另一个接受三个参数。这两个签名被称为重载签名

然后,我们编写了一个具有兼容签名的函数实现。函数具有实现签名,但此签名不能直接调用。即使我们编写了一个带有两个可选参数(在必需参数之后)的函数,也不能用两个参数调用它!

重载签名和实现签名

这是一个常见的混淆来源。人们经常会编写这样的代码,却不明白为什么会出现错误。

ts
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
Expected 1 arguments, but got 0.2554Expected 1 arguments, but got 0.
Try

同样,用于编写函数体的签名在外部是“不可见”的。

实现的签名在外部是不可见的。在编写重载函数时,您应该始终在函数实现之上有两个或更多个签名。

实现签名也必须与重载签名兼容。例如,以下函数存在错误,因为实现签名与重载不匹配。

ts
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
This overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
Try
ts
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
This overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.
function fn(x: string | number) {
return "oops";
}
Try

编写良好的重载

与泛型类似,在使用函数重载时,您应该遵循一些准则。遵循这些原则将使您的函数更易于调用、更易于理解和更易于实现。

让我们考虑一个返回字符串或数组长度的函数

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

此函数很好;我们可以用字符串或数组调用它。但是,我们不能用可能是字符串数组的值调用它,因为 TypeScript 只能将函数调用解析为单个重载。

ts
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type 'string' is not assignable to type 'any[]'.2769No overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type 'string' is not assignable to type 'any[]'.
Try

由于两个重载具有相同的参数计数和相同的返回类型,因此我们可以改为编写一个非重载版本的函数

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

这样好多了!调用者可以使用任何一种类型调用它,并且作为额外的好处,我们不必确定正确的实现签名。

尽可能使用联合类型参数,而不是重载。

在函数中声明this

TypeScript 将通过代码流分析推断函数中this 应该是什么,例如在以下情况下

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

TypeScript 理解函数user.becomeAdmin 具有相应的this,即外部对象userthis呵呵,对于很多情况来说已经足够了,但是有很多情况需要您对this 代表的对象进行更多控制。JavaScript 规范规定您不能使用名为this 的参数,因此 TypeScript 使用该语法空间让您在函数体中声明this 的类型。

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

这种模式在回调风格的 API 中很常见,其中另一个对象通常控制何时调用你的函数。请注意,你需要使用 `function` 而不是箭头函数来获得这种行为。

ts
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(() => this.admin);
The containing arrow function captures the global value of 'this'.
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
7041
7017
The containing arrow function captures the global value of 'this'.
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
Try

其他需要了解的类型

在使用函数类型时,你会经常遇到一些额外的类型。像所有类型一样,你可以在任何地方使用它们,但这些类型在函数的上下文中尤其相关。

void

void 表示不返回值的函数的返回值。当函数没有 `return` 语句,或者没有从这些 `return` 语句中返回任何显式值时,它就是推断的类型。

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

在 JavaScript 中,不返回值的函数将隐式返回 `undefined` 值。但是,在 TypeScript 中,`void` 和 `undefined` 不是同一个东西。本章末尾有更多细节。

void 与 `undefined` 不相同。

object

特殊类型object指的是任何不是基本类型(stringnumberbigintbooleansymbolnullundefined)的值。这与空对象类型{ }不同,也与全局类型Object不同。你很可能永远不会使用Object

object不是Object始终使用object

请注意,在 JavaScript 中,函数值是对象:它们具有属性,在它们的原型链中具有Object.prototype,是instanceof Object,你可以对它们调用Object.keys,等等。出于这个原因,函数类型在 TypeScript 中被认为是object

unknown

unknown类型表示任何值。这类似于any类型,但更安全,因为对unknown值执行任何操作都是非法的。

ts
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
'a' is of type 'unknown'.18046'a' is of type 'unknown'.
}
Try

这在描述函数类型时很有用,因为你可以描述接受任何值的函数,而无需在函数体中使用any值。

相反,你可以描述一个返回未知类型值的函数。

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

never

有些函数永远不会返回值。

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

never类型表示永远不会观察到的值。在返回类型中,这意味着函数抛出异常或终止程序执行。

当 TypeScript 确定联合中没有剩余内容时,也会出现never

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

Function

全局类型Function描述了所有 JavaScript 函数值中存在的bindcallapply等属性。它还具有一个特殊属性,即类型为Function的值始终可以被调用;这些调用返回any

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

这是一个无类型函数调用,通常最好避免,因为其返回值类型为不安全的any

如果您需要接受任意函数但不想调用它,类型() => void 通常更安全。

剩余参数和参数

背景阅读
剩余参数
扩展语法

剩余参数

除了使用可选参数或重载来创建可以接受各种固定参数数量的函数外,我们还可以使用剩余参数定义接受无限数量参数的函数。

剩余参数出现在所有其他参数之后,并使用...语法

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

在 TypeScript 中,这些参数的类型注释隐式为any[] 而不是any,并且给定的任何类型注释必须是Array<T>T[] 形式,或元组类型(我们将在后面学习)。

剩余参数

相反,我们可以使用扩展语法从可迭代对象(例如数组)中提供可变数量的参数。例如,数组的push 方法接受任意数量的参数

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

请注意,通常情况下,TypeScript 不会假设数组是不可变的。这可能会导致一些令人惊讶的行为

ts
// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);
A spread argument must either have a tuple type or be passed to a rest parameter.2556A spread argument must either have a tuple type or be passed to a rest parameter.
Try

针对这种情况的最佳解决方案取决于您的代码,但通常情况下,const 上下文是最直接的解决方案。

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

使用剩余参数可能需要在针对旧版运行时时启用 downlevelIteration

参数解构

背景阅读
解构赋值

您可以使用参数解构将作为参数提供的对象方便地解包到函数体中的一个或多个局部变量中。在 JavaScript 中,它看起来像这样

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

对象的类型注释放在解构语法之后

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

这可能看起来有点冗长,但您也可以在这里使用命名类型

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

函数的可赋值性

返回类型 void

函数的 void 返回类型可能会产生一些不寻常但预期的行为。

带有 `void` 返回类型的上下文类型 **不会** 强制函数 **不** 返回任何值。换句话说,带有 `void` 返回类型的上下文函数类型(`type voidFunc = () => void`),在实现时,可以返回 *任何* 其他值,但该值会被忽略。

因此,以下 `() => void` 类型的实现都是有效的

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

当这些函数的返回值被赋值给另一个变量时,它将保留 `void` 类型。

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

这种行为的存在是为了使以下代码有效,即使 `Array.prototype.push` 返回一个数字,而 `Array.prototype.forEach` 方法期望一个返回类型为 `void` 的函数。

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

还有一个特殊情况需要注意,当字面量函数定义具有 `void` 返回类型时,该函数 **不能** 返回任何值。

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

有关 `void` 的更多信息,请参阅以下其他文档条目

TypeScript 文档是一个开源项目。帮助我们改进这些页面 通过发送 Pull Request

贡献者
RCRyan Cavanaugh (56)
OTOrta Therox (15)
HAHossein Ahmadian-Yazdi (4)
JWJoseph Wynn (3)
SBSiarhei Bobryk (2)
34+

上次更新时间:2024 年 3 月 21 日