关于函数的更多内容
函数是任何应用程序的基本构建块,无论是局部函数、从其他模块导入的函数,还是类上的方法。 它们也是值,就像其他值一样,TypeScript 有许多方法来描述如何调用函数。 让我们学习如何编写描述函数的类型。
函数类型表达式
描述函数的最简单方法是使用函数类型表达式。 这些类型在语法上类似于箭头函数:
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类型参数的函数”!
当然,我们可以使用类型别名来命名函数类型:
调用签名
在 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);Try请注意,语法与函数类型表达式略有不同——在参数列表和返回类型之间使用 : 而不是 =>。
构造签名
JavaScript 函数也可以使用 new 运算符调用。 TypeScript 将这些称为构造函数,因为它们通常创建一个新对象。 您可以通过在调用签名前添加 new 关键字来编写一个构造签名:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}Try某些对象,例如 JavaScript 的 Date 对象,可以使用或不使用 new 调用。 您可以在同一类型中任意组合调用签名和构造签名:
interface CallOrConstruct {
(n?: number): string;
new (s: string): Date;
}
function fn(ctor: CallOrConstruct) {
// 向 `ctor` 传递一个类型为 `number` 的参数,使其与
// `CallOrConstruct` 接口中的第一个定义匹配。
console.log(ctor(10));
// 类似地,向 `ctor` 传递一个类型为 `string` 的参数,使其与
// `CallOrConstruct` 接口中的第二个定义匹配。
console.log(new ctor("10"));
}
fn(Date);Try泛型函数
编写一个函数是很常见的,其中输入的类型与输出的类型相关,或者两个输入的类型以某种方式相关。 让我们考虑一个返回数组第一个元素的函数:
这个函数完成了它的工作,但不幸的是返回类型是 any。 如果函数返回数组元素的类型会更好。
在 TypeScript 中,当我们想要描述两个值之间的对应关系时,会使用泛型。 我们通过在函数签名中声明一个类型参数来做到这一点:
通过向此函数添加类型参数 Type 并在两个地方使用它,我们在函数的输入(数组)和输出(返回值)之间创建了一个链接。 现在当我们调用它时,会输出一个更具体的类型:
// s 的类型是 'string'
const s = firstElement(["a", "b", "c"]);
// n 的类型是 'number'
const n = firstElement([1, 2, 3]);
// u 的类型是 undefined
const u = firstElement([]);Try推断
请注意,在此示例中我们不必指定 Type。 类型是由 TypeScript 推断——自动选择的。
我们也可以使用多个类型参数。 例如,map 的独立版本如下所示:
// prettier-ignore
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// 参数 'n' 的类型是 'string'
// 'parsed' 的类型是 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));Try请注意,在此示例中,TypeScript 既可以推断 Input 类型参数的类型(来自给定的 string 数组),也可以根据函数表达式的返回值(number)推断 Output 类型参数。
约束
我们编写了一些泛型函数,它们可以处理任何类型的值。 有时我们想要关联两个值,但只能操作某个值的子集。 在这种情况下,我们可以使用约束来限制类型参数可以接受的类型种类。
让我们编写一个返回两个值中较长者的函数。 为此,我们需要一个作为数字的 length 属性。 我们通过编写 extends 子句来约束类型参数为该类型:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray 的类型是 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 的类型是 'alice' | 'bob'
const longerString = longest("alice", "bob");
// 错误!数字没有 'length' 属性
const notOK = longest(10, 100);Try在这个例子中有一些有趣的事情需要注意。 我们允许 TypeScript 推断 longest 的返回类型。 返回类型推断也适用于泛型函数。
因为我们将 Type 约束为 { length: number },所以我们被允许访问 a 和 b 参数的 .length 属性。 如果没有类型约束,我们将无法访问这些属性,因为这些值可能是其他没有 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 }; }
}Try这个函数看起来可能没问题——Type 被约束为 { length: number },并且函数要么返回 Type,要么返回与该约束匹配的值。 问题在于该函数承诺返回与传入的相同类型的对象,而不仅仅是某个匹配约束的对象。 如果这段代码是合法的,你可以编写肯定不起作用的代码:
// 'arr' 获取值 { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// 并在这里崩溃,因为数组有
// 'slice' 方法,但返回的对象没有!
console.log(arr.slice(0));Try指定类型参数
TypeScript 通常可以在泛型调用中推断出预期的类型参数,但并非总是如此。 例如,假设您编写了一个组合两个数组的函数:
通常,使用不匹配的数组调用此函数会是一个错误:
但是,如果您打算这样做,您可以手动指定 Type:
编写优秀泛型函数的指南
编写泛型函数很有趣,并且很容易过度使用类型参数。 使用过多的类型参数或在不必要的地方使用约束会使推断成功率降低,让函数的调用者感到沮丧。
向下推送类型参数
以下是两种看似相似的函数编写方式:
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (良好)
const a = firstElement1([1, 2, 3]);
// b: any (不良)
const b = firstElement2([1, 2, 3]);Try乍一看它们可能相同,但 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);
}Try我们创建了一个类型参数 Func,它不关联两个值。 这总是一个危险信号,因为这意味着希望指定类型参数的调用者必须无缘无故地手动指定一个额外的类型参数。 Func 没有做任何事情,只会使函数更难阅读和推理!
规则:始终使用尽可能少的类型参数
类型参数应该出现两次
有时我们会忘记一个函数可能不需要是泛型的:
我们可以轻松地编写一个更简单的版本:
记住,类型参数用于关联多个值的类型。 如果类型参数在函数签名中只使用一次,那么它没有关联任何内容。 这包括推断的返回类型;例如,如果 Str 是 greet 推断返回类型的一部分,那么它将关联参数和返回类型,因此在编写的代码中虽然只出现一次,但会被使用两次。
规则:如果类型参数只出现在一个位置,请重新考虑是否真的需要它
可选参数
JavaScript 中的函数通常接受可变数量的参数。 例如,number 的 toFixed 方法接受一个可选的小数位数:
function f(n: number) {
console.log(n.toFixed()); // 0 个参数
console.log(n.toFixed(3)); // 1 个参数
}Try我们可以在 TypeScript 中通过使用 ? 将参数标记为可选来对此进行建模:
尽管参数被指定为 number 类型,但 x 参数实际上将具有类型 number | undefined,因为 JavaScript 中未指定的参数会获得值 undefined。
您还可以提供参数默认值:
现在在 f 的函数体中,x 将具有类型 number,因为任何 undefined 参数都将被替换为 10。 请注意,当参数可选时,调用者始终可以传递 undefined,因为这简单地模拟了一个“缺失”的参数:
回调中的可选参数
一旦您了解了可选参数和函数类型表达式,在编写调用回调的函数时就很容易犯以下错误:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}Try人们通常在编写 index? 作为可选参数时的意图是希望这两个调用都是合法的:
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));Try这实际上意味着callback 可能只用一个参数调用。 换句话说,函数定义表明实现可能如下所示:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// 我今天不想提供索引
callback(arr[i]);
}
}Try反过来,TypeScript 将强制执行此含义并发出并非真正可能的错误:
在 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);Try在此示例中,我们编写了两个重载:一个接受一个参数,另一个接受三个参数。 前两个签名称为重载签名。
然后,我们编写了一个具有兼容签名的函数实现。 函数有一个实现签名,但这个签名不能直接调用。 即使我们编写了一个在必需参数之后有两个可选参数的函数,它也不能用两个参数调用!
重载签名与实现签名
这是一个常见的困惑来源。 人们经常编写这样的代码,并且不理解为什么会出现错误:
再次强调,用于编写函数体的签名在外部是“看不见”的。
实现的签名从外部是不可见的。 在编写重载函数时,您应该始终在函数实现之上拥有两个或更多签名。
实现签名也必须与重载签名兼容。 例如,这些函数有错误,因为实现签名没有以正确的方式匹配重载:
function fn(x: boolean): void;
// 参数类型不正确
function fn(x: string): void;function fn(x: boolean) {}Tryfunction fn(x: string): string;
// 返回类型不正确
function fn(x: number): boolean;function fn(x: string | number) {
return "oops";
}Try编写良好的重载
像泛型一样,在使用函数重载时您应该遵循一些准则。 遵循这些原则将使您的函数更容易调用、更容易理解,也更容易实现。
让我们考虑一个返回字符串或数组长度的函数:
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}Try这个函数没问题;我们可以用字符串或数组调用它。 但是,我们不能使用可能是字符串或数组的值来调用它,因为 TypeScript 只能将函数调用解析为单个重载:
由于两个重载具有相同的参数数量和相同的返回类型,我们可以改为编写该函数的非重载版本:
这样好多了! 调用者可以使用任何一种值来调用它,而且额外的好处是,我们不必弄清楚正确的实现签名。
如果可能,始终优先使用联合类型的参数而不是重载
在函数中声明 this
TypeScript 将通过代码流分析推断函数中 this 应该是什么,例如在以下示例中:
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;
});Try这种模式在回调风格的 API 中很常见,其中另一个对象通常控制函数的调用时间。请注意,您需要使用 function 而不是箭头函数来获得此行为:
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(() => this.admin);Try需要了解的其他类型
您还会遇到一些在处理函数类型时经常出现的其他类型。 像所有类型一样,您可以在任何地方使用它们,但这些类型在函数的上下文中尤其相关。
void
void 表示不返回值的函数的返回值。 它是当函数没有任何 return 语句,或从这些 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,等等。 因此,函数类型在 TypeScript 中被视为 object。
unknown
unknown 类型表示任何值。 这类似于 any 类型,但更安全,因为对 unknown 值执行任何操作都是不合法的:
这在描述函数类型时很有用,因为您可以描述接受任何值而无需在函数体内包含 any 值的函数。
相反,您可以描述一个返回 unknown 类型值的函数:
function safeParse(s: string): unknown {
return JSON.parse(s);
}
// 需要小心处理 'obj'!
const obj = safeParse(someRandomString);Trynever
某些函数从不返回值:
never 类型表示永远不会被观察到的值。 在返回类型中,这意味着函数抛出异常或终止程序执行。
当 TypeScript 确定联合中没有剩余内容时,never 也会出现。
function fn(x: string | number) {
if (typeof x === "string") {
// 做某事
} else if (typeof x === "number") {
// 做其他事
} else {
x; // 类型为 'never'!
}
}TryFunction
全局类型 Function 描述了诸如 bind、call、apply 等属性,这些属性存在于 JavaScript 中的所有函数值上。 它还具有一个特殊属性,即 Function 类型的值总是可以被调用;这些调用返回 any:
这是一个无类型函数调用,由于不安全的 any 返回类型,通常最好避免使用。
如果您需要接受任意函数但不打算调用它,则类型 () => void 通常更安全。
剩余参数和展开参数
剩余参数
除了使用可选参数或重载来使函数接受各种固定数量的参数之外,我们还可以使用剩余参数来定义接受无限数量参数的函数。
剩余参数出现在所有其他参数之后,并使用 ... 语法:
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' 获取值 [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);Try在 TypeScript 中,这些参数上的类型注解隐式是 any[] 而不是 any,并且任何给定的类型注解必须是 Array<T> 或 T[] 的形式,或者是元组类型(我们稍后会学习)。
展开参数
相反,我们可以使用展开语法从一个可迭代对象(例如数组)提供可变数量的参数。 例如,数组的 push 方法接受任意数量的参数:
请注意,通常 TypeScript 不会假设数组是不可变的。 这可能会导致一些令人惊讶的行为:
// 推断类型是 number[] -- "一个包含零个或多个数字的数组",
// 而不是特定的两个数字
const args = [8, 5];
const angle = Math.atan2(...args);Try这种情况的最佳解决方案取决于您的代码,但通常 const 上下文是最直接的解决方案:
当目标为较旧的运行时环境时,使用展开参数可能需要开启 downlevelIteration。
参数解构
背景阅读:
解构赋值
您可以使用参数解构方便地将作为参数提供的对象解包到函数体中的一个或多个局部变量中。 在 JavaScript 中,它看起来像这样:
function sum({ a, b, c }) {
console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });对象的类型注解位于解构语法之后:
这看起来有点冗长,但您也可以在这里使用命名类型:
// 与之前的示例相同
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 类型的实现是有效的:
type voidFunc = () => void;
const f1: voidFunc = () => {
return true;
};
const f2: voidFunc = () => true;
const f3: voidFunc = function () {
return true;
};Try并且当这些函数之一的返回值赋值给另一个变量时,它将保留 void 类型:
存在这种行为是为了使以下代码有效,即使 Array.prototype.push 返回一个数字,而 Array.prototype.forEach 方法期望一个返回类型为 void 的函数。
还有一个特殊情况需要注意,当一个字面函数定义具有 void 返回类型时,该函数不得返回任何内容。
function f2(): void {
// @ts-expect-error
return true;
}
const f3 = function (): void {
// @ts-expect-error
return true;
};Try有关 void 的更多信息,请参考其他文档条目: