TypeScript学习之函数

函数

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

约束条件

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

让我们编写一个返回两个值中较长者的函数。 为此,我们需要一个 length 属性,它是一个数字。 我们通过编写 extends 子句将类型参数限制为该类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 },所以我们可以访问 ab 参数的 .length 属性。 如果没有类型约束,我们将无法访问这些属性,因为这些值可能是没有长度属性的其他类型。

longerArraylongerString 的类型是根据参数推断出来的。 请记住,泛型就是将两个或多个具有相同类型的值关联起来!

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

使用约束值

这是使用通用约束时的一个常见错误:

1
2
3
4
5
6
7
8
9
10
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 或与该约束匹配的值。 问题是该函数承诺返回与传入相同类型的对象,而不仅仅是与约束匹配的某个对象。 如果这段代码是合法的,你可以编写绝对行不通的代码:

1
2
3
4
5
6
7
8
9
10
declare function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: 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 通常可以在泛型调用中推断出预期的类型参数,但并非总是如此。 例如,假设您编写了一个函数来组合两个数组:

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

通常使用不匹配的数组调用此函数会出错:

1
2
3
declare function combine<Type>(arr1: Type[], arr2: Type[]): Type[];

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

但是,如果您打算这样做,您可以手动指定 Type

1
2
3
declare function combine<Type>(arr1: Type[], arr2: Type[]): Type[];

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

可选参数

JavaScript 中的函数通常采用可变数量的参数。 例如,numbertoFixed 方法采用可选的位数计数:

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

我们可以通过使用 ? 将参数标记为可选来在 TypeScript 中对此进行建模:

1
2
3
4
5
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK

尽管参数被指定为 number 类型,但 x 参数实际上将具有 number | undefined 类型,因为 JavaScript 中未指定的参数获取值 undefined

您还可以提供参数默认值:

1
2
3
function f(x = 10) {
// ...
}

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

1
2
3
4
5
6
declare function f(x?: number): void;
// cut
// All OK
f();
f(10);
f(undefined);

回调中的可选参数

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

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

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

1
2
3
4
5
6
7
declare function myForEach(
arr: any[],
callback: (arg: any, index?: number) => void
): void;

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

这实际上意味着 callback 可能会被一个参数调用。 换句话说,函数定义表明实现可能如下所示:

1
2
3
4
5
6
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 将强制执行此含义并发出实际上不可能的错误:

1
2
3
4
5
6
7
8
declare function myForEach(
arr: any[],
callback: (arg: any, index?: number) => void
): void;

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

在 JavaScript 中,如果你调用一个参数多于参数的函数,多余的参数将被忽略。 TypeScript 的行为方式相同。 具有较少参数(相同类型)的函数总是可以代替具有更多参数的函数。

为回调编写函数类型时,切勿编写可选参数,除非您打算在不传递该参数的情况下调用该函数

函数重载

可以以各种参数计数和类型调用一些 JavaScript 函数。 例如,您可以编写一个函数来生成一个 Date,它采用时间戳(一个参数)或月/日/年规范(三个参数)。

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

1
2
3
4
5
6
7
8
9
10
11
12
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);

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

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

重载签名和实现签名

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

1
2
3
4
5
6
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();

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

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

实现签名还必须与重载签名兼容。 例如,这些函数有错误,因为实现签名没有以正确的方式匹配重载:

1
2
3
4
5
6
7
8
9
10
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";
}

编写好的重载

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

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

1
2
3
4
5
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}

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

1
2
3
4
5
6
declare function len(s: string): number;
declare function len(arr: any[]): number;

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

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

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

这好多了! 调用者可以使用任何一种值来调用它,作为额外的好处,我们不必找出正确的实现签名。

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

其他需要了解的类型

在使用函数类型时,您需要识别一些经常出现的其他类型。 像所有类型一样,您可以在任何地方使用它们,但这些在函数的上下文中尤其相关。

void

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

1
2
3
4
// The inferred return type is void
function noop() {
return;
}

在 JavaScript 中,不返回任何值的函数将隐式返回值 undefined。 但是,voidundefined 在 TypeScript 中不是一回事。 本章末尾有更多详细信息。

voidundefined 不同。

object

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

object 不是 Object总是使用 object

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

unknown

unknown 类型代表任何值。 这类似于 any 类型,但更安全,因为使用 unknown 值做任何事情都是不合法的:

1
2
3
4
5
6
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
}

这在描述函数类型时很有用,因为您可以描述接受任何值而不在函数体中包含 any 值的函数。

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

1
2
3
4
5
6
7
8
declare const someRandomString: string;

function safeParse(s: string): unknown {
return JSON.parse(s);
}

// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);

never

有些函数从不返回值:

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

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

当 TypeScript 确定联合中没有任何内容时,never 也会出现。

1
2
3
4
5
6
7
8
9
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 描述了 bindcallapply 等属性,以及 JavaScript 中所有函数值上的其他属性。 它还有一个特殊的属性,即 Function 类型的值总是可以被调用;这些调用返回 any

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

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

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

剩余形参和实参

剩余形参

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

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

1
2
3
4
5
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 方法接受任意数量的参数:

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

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

1
2
3
4
// 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 上下文是最直接的解决方案:

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

针对较旧的运行时,使用剩余参数可能需要打开 downlevelIteration

参数解构

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

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

对象的类型注释遵循解构语法:

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

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

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