Skip to content

TypeScript 中的函数

函数类型表达式

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

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

当然,我们可以使用 type alias(别名) 来命名函数 type:

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

函数签名

也叫类型签名,或方法签名,定义了函数或方法的输入、输出、函数的属性。

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

上面代码定义了:函数或方法的输入、输出、函数的属性。

构造签名

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

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

函数泛型

泛型可以理解为,类型变量。

typescript
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
const s = firstElement(["a", "b", "c"]); // s is of type 'string'
const n = firstElement([1, 2, 3]);       // n is of type 'number'
const u = firstElement([]);              // u is of type undefined
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
const s = firstElement(["a", "b", "c"]); // s is of type 'string'
const n = firstElement([1, 2, 3]);       // n is of type 'number'
const u = firstElement([]);              // u is of type undefined

泛型推理行为

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

我们也可以使用多个类型参数。例如,map 的独立版本如下所示:

typescript
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

请注意,在此示例中,TypeScript 可以根据函数表达式的返回值 (number) 推断 Input type 参数的类型(从给定的字符串数组),以及 Output type 参数。

泛型约束行为

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

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

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

T extends { length: number },这里定义的泛型 T 不但代表类型变量,这个类型变量还继承了一个 { length: number }属性。

泛型就是将两个或多个值与同一类型相关联

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

typescript
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}
const arr = combine([1, 2, 3], ["hello"]);
// 报错:Type 'string' is not assignable to type 'number'.
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}
const arr = combine([1, 2, 3], ["hello"]);
// 报错:Type 'string' is not assignable to type 'number'.

如果您打算执行此作,则可以手动指定 Type:

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

编写良好泛型函数的准则

  1. Push Type 参数
typescript
function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}

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

const a = firstElement1([1, 2, 3]);// a: number (good)
const b = firstElement2([1, 2, 3]);// b: any (bad)
  1. 使用较少的类型参数
typescript
// (good)
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}
// (bad)
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}
// (good)
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}
// (bad)
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}
  1. 类型参数应出现两次
typescript
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

// 改写为更简单版本:Str没有存下的意义
function greet(s: string) {
  console.log("Hello, " + s);
}
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

// 改写为更简单版本:Str没有存下的意义
function greet(s: string) {
  console.log("Hello, " + s);
}

请记住,类型参数用于关联多个值的类型。如果类型参数在函数签名中只使用一次,则它与任何内容无关。这包括推断的返回类型;例如,如果 Str 是 greet 的推断返回类型的一部分,它将关联参数和返回类型,因此尽管在编写的代码中只出现过一次,但会使用两次。

规则:如果类型参数只出现在一个位置,请强烈重新考虑是否确实需要它。

可选参数

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

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

注意:回调中的可选参数

在编写调用回调的函数时很容易犯以下错误:

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

人们在编写 index?时通常打算做什么?作为可选参数,他们希望这两个调用都是合法的:

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

这实际上意味着 callback 可能会用一个参数来调用。

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

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

typescript
myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
});
// 报错:'i' is possibly 'undefined'.
myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
});
// 报错:'i' is possibly 'undefined'.

在 JavaScript 中,如果你调用的函数的参数多于参数数,则额外的参数将被忽略。TypeScript 的行为方式相同。参数较少(相同类型)的函数始终可以代替参数较多的函数。

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

函数重载

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

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

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);  //报错: 参数数量不对
// No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
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.
编写重载函数时,应始终在函数实现上方有两个或多个签名。实现签名还必须与重载签名兼容

编写好的重载函数

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

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

typescript
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); 
// 报错
// 因为 TypeScript 只能将函数调用解析为单个重载:
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); 
// 报错
// 因为 TypeScript 只能将函数调用解析为单个重载:

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

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

TIP

尽可能始终首选具有联合类型的参数,而不是重载

函数中的this

TypeScript 将通过代码流分析推断函数中的 this 应该是什么

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

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

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

这种模式在回调样式的 API 中很常见,其中另一个对象通常控制函数的调用时间。请注意,您需要使用 function 而不是 arrow 函数来获得此行为。

其他类型

void:在 JavaScript 中,不返回任何值的函数将隐式返回值 undefined。但是,void 和 undefined 在 TypeScript 中不是一回事。 object:object 不是 Object。始终使用 object。 unknown:unknown 类型表示任何值。这类似于 any 类型,但更安全,因为对未知值执行任何作都是不合法的。 never:类型表示从未观察到的值。在返回类型中,这意味着函数引发异常或终止程序的执行。never 当 TypeScript 确定 union 中没有剩余内容时,也会出现。 Function:全局类型 Function 描述了 JavaScript 中所有函数值上存在的属性,如 bind、call、apply 和其他属性。它还具有始终可以调用 Function 类型的值的特殊属性;这些调用返回 any。

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

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

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

Rest 参数和参数

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

typescript
function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
const a = multiply(10, 1, 2, 3, 4);
function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
const a = multiply(10, 1, 2, 3, 4);

在 TypeScript 中,这些参数的类型注释隐式是 any[] 而不是 any,并且给出的任何类型注释都必须是 Array<T>T[] 的形式,或者是元组类型

TypeScript 不假定数组是不可变的。这可能会导致一些令人惊讶的行为:

typescript
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.
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.

这种情况的最佳解决方法在一定程度上取决于你的代码,但一般来说,const 上下文是最直接的解决方案:

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

参数解构

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

typescript
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);
}

type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}
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);
}

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

函数的可分配性

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

return 类型为 void 的上下文类型不会强制函数不返回某些内容。另一种说法是具有 void 返回类型(类型 voidFunc = () => void)的上下文函数类型,实现后,可以返回任何其他值,但它将被忽略。