第 5 章 函数

Flying
2024-01-28 / 0 评论 / 37 阅读 / 正在检测是否收录...

函数参数*
一端输入,另一端输出
作为返回类型

在第 2 章 “类型系统” 中,你了解了如何使用类型注解来注解变量的值。现在,你将了解如何对函数参数和返回类型执行相同的操作,以及为什么这会很有用。

函数参数

以下面的sing函数为例,它接收一个 song 参数并记录它 :

function sing(song) { 
  console.log(`Singing: ${song}!`);
}

编写 sing 函数的开发人员打算为 song 参数提供什么值类型?

string 吗?它是具有覆盖 toString() 方法的对象吗?这个代码有问题吗?谁知道呢?!

如果没有声明显式的类型信息,我们可能永远不会知道——TypeScript 会将其视为 any 类型,这意味着参数的类型可以是任何类型。

与变量一样,TypeScript 允许你使用类型注解声明函数参数的类型。现在我们可以使用 : string 来告诉 TypeScript song 参数的类型为 string

function sing(song: string) { 
  console.log(`Singing: ${song}!`);
}

太好了:现在我们知道 song 是什么类型了!

请注意,你不需要为函数参数添加正确的类型注解以使代码成为有效的 TypeScript 语法。TypeScript 可能会向你发出类型错误的警告,但生成的 JavaScript 代码仍然可以运行。在上一个代码片段中,如果 song 参数缺少类型声明,它仍会从TypeScript 转换为 JavaScript。第 13 章“配置选项”将介绍如何配置 TypeScript 对隐式类型 any 的参数的警告,就像 song 参数一样。

必需参数

与允许使用任意数量的参数调用函数的 JavaScript 不同,TypeScript 假定在函数上声明的所有参数都是必需的。如果使用错误数量的参数调用函数,TypeScript 将抛出一个类型错误。如果调用一个函数的参数太少或太多,TypeScript 的参数计数就会发挥作用。

singTwo 函数需要两个参数,因此不允许传递一个参数和传递三个参数:

function singTwo(first: string, second: string) { 
  console.log(`${first} / ${second}`);
}


// 日志:"Ball and Chain / undefined"
singTwo("Ball and Chain");
// ~~~~
// 错误:应有 2 个参数,但获得 1 个。

// 日志:"I Will Survive / Higher Love"
singTwo("I Will Survive", "Higher Love"); // Ok

// 日志:"Go Your Own Way / The Chain"
singTwo("Go Your Own Way", "The Chain", "Dreams");
// ~~~~
// 错误:应有 2 个参数,但获得 3 个。

强制向函数提供必需的参数,有助于通过确保函数内存在所有预期的参数值来强制实施类型安全。未能确保这些值存在可能会导致代码中出现意外行为,例如之前的singTwo 函数记录 undefined 或忽略参数。

形参(Parameter)是指函数对它期望作为参数接收的内容的声明。实参(Argument)是指在函数调用中提供给形参的值。在前面的示例中,first 和 第second 是形参,而“Dreams”等字符串是实参。

可选参数

回想一下,在 JavaScript 中,如果未提供函数参数,则函数中的参数值默认为 undefined。有时不需要提供函数参数,函数的预期用途是针对该 undefined 值。我们不希望 TypeScript 因为未能提供可选参数报类型错误。TypeScript 允许通过在参数的类型注解中的 : 之前添加 ? 来将参数注解为可选参数,类似于可选对象类型属性。

函数调用时不需要提供可选参数。因此,它们的类型总是将 | undefined 添加为联合类型。

在下面的 announceSong 函数中,singer 参数标记为可选。其类型为 string | undefined,并且不需要由函数的调用方提供。如果提供了 singer,则可能是 string 值或 undefined

function announceSong(song: string, singer?: string) {
  console.log(`Song: ${song}`);
  if (singer) {
    console.log(`Singer: ${singer}`);
  }
}

announceSong("Greensleeves"); // Ok 
announceSong("Greensleeves", undefined); // Ok 
announceSong("Chandelier", "Sia"); // Ok

这些可选参数总是可以隐式为 undefined。在前面的代码中,singer一开始的类型是 string | undefined,然后通过 if 语句缩小到仅 string

可选参数与恰好包含 | undefined 的联合类型的参数不同。后者必须提供未使用 ? 标记为可选的参数,即使该值显式声明为 undefined

这个 announceSongBy 函数中的 singer 参数必须显式提供。它可能是 string 值或 undefined

function announceSongBy(song: string, singer: string | undefined) { /* ... */ }

announceSongBy("Greensleeves");
// 错误:应有 2 个参数,但获得 1 个。

announceSongBy("Greensleeves", undefined); // Ok
announceSongBy("Chandelier", "Sia"); // Ok

函数的任何可选参数必须是最后一个参数。将可选参数放在必需参数之前将触发 TypeScript 语法错误:

function announceSinger(singer?: string, song: string) {}
// ~~~~~~~
// 错误:必选参数不能位于可选参数后。

默认参数

JavaScript中的可选参数可以通过=和声明中的值来指定默认值。对于这些可选参数,由于默认情况下提供了值,因此其 TypeScript 类型不会在函数内部隐式地添加加 | undefined 联合。TypeScript 仍然允许在缺少或 undefined 参数的情况下调用函数。

TypeScript 对函数默认参数值的类型推断与对变量初始值的类型推断类似。如果参数有默认值但没有类型注解,则 TypeScript 将根据该默认值推断参数的类型。

在下面的 rateSong 函数中, rating 被推断为 number 类型,但在调用该函数的代码中是可选的 number | undefined

function rateSong(song: string, rating = 0) {
  console.log(`${song} gets ${rating}/5 stars!`);
}

rateSong("Photograph"); // Ok
rateSong("Set Fire to the Rain", 5); // Ok
rateSong("Set Fire to the Rain", undefined); // Ok

rateSong("At Last!", "100");
// ~~~~~~~
// 错误:类型“string”的参数不能赋给类型“number”的参数。

剩余参数

JavaScript 中的某些函数可以使用任意数量的参数进行调用。 展开运算符可以放在函数声明中的最后一个参数上,表示传递给从该参数开始的函数的任何 “剩余” 参数都应存储在一个数组中。

TypeScript 允许像常规参数一样声明这些剩余参数的类型,只是在末尾添加了 [] 语法来表明它是一个参数数组。

在这里,允许 singAllTheSongs 为其 songs 剩余参数获取零个或多个 string 类型的参数:

function singAllTheSongs(singer: string, ...songs: string[]) {
  for (const song of songs) {
    console.log(`${song}, by ${singer}`);
  }
}

singAllTheSongs("Alicia Keys"); // Ok
singAllTheSongs("Lady Gaga", "Bad Romance", "Just Dance", "Poker Face"); // Ok

singAllTheSongs("Ella Fitzgerald", 2000);
// ~~~~~~~
// 错误:类型“number”的参数不能赋给类型“string”的参数。

我将在第 6 章 “数组”中介绍在 TypeScript 中使用数组。

返回类型

TypeScript 是敏锐的:如果它理解函数返回的所有可能值,它就会知道函数返回的类型。在此示例中,TypeScript 将 singSongs 理解为返回 number

// 类型:(songs: string[]) => number
function singSongs(songs: string[]) {
  for (const song of songs) {
    console.log(`${song}`);
  }

  return songs.length;
}

如果函数包含多个具有不同值的 return 语句,TypeScript 将推断返回类型是所有可能的返回类型的联合。

getSongAt 函数将推断为返回 string | undefined,因为它的两个可能的返回值类型分别是 stringundefined

// 类型:(songs: string[], index: number) => string | undefined
function getSongAt(songs: string[], index: number) {
  return index < songs.length
    ? songs[index]
    : undefined;
}

显式返回类型

与变量一样,我通常建议不要费心使用类型注解显式声明函数的返回类型。但是,在少数情况下,它可以专门用于函数:

  • 你可能希望强制具有许多可能返回值的函数始终返回相同类型的值。
  • TypeScript 将拒绝尝试通过递归函数的返回类型进行推理。
  • 它可以加快非常大的项目中的 TypeScript 类型检查,即那些拥有数百个或更多 TypeScript 文件的项目。

函数声明返回类型注解放在参数列表后面的 ) 之后。对于函数声明,它正好在 { 之前:

function singSongsRecursive(songs: string[], count = 0): number {
    return songs.length ? singSongsRecursive(songs.slice(1), count + 1) : count;
}

对于箭头函数(也称为 lambda),它正好在 => 之前:

const singSongsRecursive = (songs: string[], count = 0): number =>
  songs.length ? singSongsRecursive(songs.slice(1), count + 1) : count;

如果函数中的 return 语句返回的值不可分配给函数的返回类型,TypeScript 将抛出一个可分配性错误。

在这里,getSongRecordingDate 函数被显式声明为返回 Date | undefined,但它的一个返回语句错误地提供了 string

function getSongRecordingDate(song: string): Date | undefined {
  switch (song) {
    case "Strange Fruit":
      return new Date('April 20, 1939'); // Ok

    case "Greensleeves":
      return "unknown";
    // 错误:不能将类型“string”分配给类型“Date”。

    default:
      return undefined; // Ok
  }
}

函数类型

JavaScript 允许我们将函数作为值传递。这意味着我们需要一种方法来声明用于保存函数的参数或变量的类型。

函数类型语法看起来类似于箭头函数,但使用的是类型而不是主体。

这个 nothingInGivesString 变量的类型描述一个没有参数和返回 string 值的函数:

let nothingInGivesString: () => string;

下述 inputAndOutput 变量的类型描述一个带有 string[] 参数、count 可选参数和返回值为 number 的函数:

let inputAndOutput: (songs: string[], count?: number) => number;

函数类型经常用于描述回调参数(旨在作为函数调用的参数)。

例如,以下 runOnSongs 代码段将其 getSongAt 参数的类型声明为一个函数,该函数接受 index: number 并返回 string。传入的getSongAt 与该类型匹配,但 logSong 因为接收的参数是 string 而不是 number 而失败:

const songs = ["Juice", "Shake It Off", "What's Up"];

function runOnSongs(getSongAt: (index: number) => string) {
  for (let i = 0; i < songs.length; i += 1) {
    console.log(getSongAt(i));
  }
}

function getSongAt(index: number) {
  return `${songs[index]}`;
}

runOnSongs(getSongAt); // Ok

function logSong(song: string) {
  return `${song}`;
}

runOnSongs(logSong);
//  ~~~~~~~

// 错误:类型“(song: string) => any”的参数不能
// 赋给类型“(index: number) => string”的参数。
//   参数“song”和“index” 的类型不兼容。
//     不能将类型“number”分配给类型“string”。

runOnSongs(logSong) 的错误消息是一个包含几个层级细节的可分配性错误的例子。当 TypeScript 报告两个函数类型不可互相赋值时,通常会给出三个层级的详细信息,并增加特定层别。具体如下:

  1. 第一级缩进打印出两个函数类型。
  2. 下一级缩进指定了不匹配的部分。
  3. 最后一级缩进是对不匹配部分的精确可分配性错误。

在前面的代码片段中,这些级别是:

  1. logSongs: (strong: string) => string 是分配给 getSongAt: (index: number) => string 收件人的提供类型
  2. logSongsong 参数分配给 getSongAtindex 参数
  3. songnumber 类型不可分配给 indexstring 类型
TypeScript 的多行错误起初似乎令人生畏。最好逐行阅读它们并了解每个部分所传达的信息对理解错误大有帮助。

函数类型括号

函数类型可以放置在使用其他类型的任何位置。这包括联合类型。

在联合类型中,可以使用圆括号来表示注解的哪一部分是函数返回值,哪一部分是联合类型:

// 类型是一个返回值为联合类型 string | undefined 的函数
let returnsStringOrUndefined: () => string | undefined;
// 类型是 undefined 或一个返回值为 string 的函数
let maybeReturnsString: (() => string) | undefined;

后续章节会介绍更多的类型语法,还会介绍函数类型必须用括号括起来的地方。

参数类型推断

如果我们必须为编写的每个函数(包括用作参数的内联函数)声明参数类型,那将很麻烦。幸运的是,TypeScript 可以通过声明类型的位置推断函数中参数的类型。

大家都知道,这个 singer 变量是接收 string 类型参数的函数,因此后来分配给 singer 函数中的 song 参数也是一个 string

let singer: (song: string) => string;

singer = function (song) {
  // Type of song: string
  return Singing: `${song.toUpperCase()}`; // Ok
};

例如,这里的 songindex 参数分别由 TypeScript 推断为 string number

const songs = ["Call Me", "Jolene", "The Chain"];

// song: string
// index: number
songs.forEach((song, index) => { 
    console.log(`${song} is at index ${index}`);
});

函数类型别名

还记得第 3 章 “联合和字面量”中的类型别名吗?它们也可用于函数类型。

这个 StringToNumber 类型为函数提供别名,该函数接受 string 类型参数并返回 number,这意味着稍后可以使用它来描述变量的类型:

type StringToNumber = (input: string) => number; 

let stringToNumber: StringToNumber; 

stringToNumber = (input) => input.length; // Ok

stringToNumber = (input) => input.toUpperCase();
// ~~~~~~~
// 错误:不能将类型“string”分配给类型“number”。

同样,函数参数本身也可以使用别名引用函数类型。

以下 usesNumberToString 函数有一个参数,该参数本身就是 NumberToString 别名函数类型:

type NumberToString = (input: number) => string;

function usesNumberToString(numberToString: NumberToString) {
  console.log(`The string is: ${numberToString(1234)}`);
}

usesNumberToString((input) => `${input}! Hooray!`); // Ok

usesNumberToString((input) => input * 2);
// ~~~~~~~
// 错误:不能将类型“number”分配给类型“string”。

类型别名对于函数类型特别有用。它们可以节省大量的水平空间,不必反复写出参数和/或返回类型。

更多返回类型

现在,让我们再来看看两个返回类型:voidnever

Void 返回

有些函数并不打算返回任何值。它们可能没有 return 语句,或者只有不返回值的 return 语句。TypeScript 允许使用 void 关键字来表示这种不返回任何值的函数的返回类型。

返回类型为 void 的函数可能不会返回任何值。这个 logSong 函数声明的返回类型是 void,所以它不允许返回任何值:

function logSong(song: string | undefined): void {
  if (!song) {
      return; // Ok
  }

  console.log(`${song}`);

  return true;
  // 错误:不能将类型“boolean”分配给类型“void”。
}

void 可用作函数类型声明中的返回类型。在函数类型声明中使用时,void 表示将忽略函数返回的任何值。

例如,以下 songLogger 变量表示一个函数,该函数接受 song: string 作为参数并且不返回值:

let songLogger: (song: string) => void;

songLogger = (song) => {
  console.log(`${song}`); // 译者注:应该是 song
};

songLogger("Heart of Glass"); // Ok

请注意,尽管在 JavaScript 中,如果没有返回实际值,所有函数默认返回 undefined,但 voidundefined 不是相同的概念。void 表示函数的返回类型将被忽略,而 undefined 是一个字面量值。试图将类型为 void 的值赋给类型中包含 undefined 的值将导致类型错误:

function returnsVoid() {
  return;
}

let lazyValue: string | undefined; 

lazyValue = returnsVoid();
// 错误:不能将类型“void”分配给类型“string | undefined”。

undefinedvoid 返回值之间的区别特别适用于忽略函数传递给类型声明为 void 的位置的任何返回值。例如,数组上的内置 forEach 方法接受返回 void 的回调。提供给 forEach 的函数可以返回所需的任何值。以下 saveRecords 函数中的 records.push(record) 返回 number(数组的 .push() 返回的值),但仍允许作为传递给 newRecords.forEach 的箭头函数的返回值:

const records: string[] = [];

function saveRecords(newRecords: string[]) {
  newRecords.forEach(record => records.push(record));
}

saveRecords(['21', 'Come On Over', 'The Bodyguard'])

void 类型不属于 JavaScript。它是一个 TypeScript 关键字,用于声明函数的返回类型。请记住,它表示函数的返回值不打算使用,而不是可以自己返回的值。

Never 返回

有些函数不仅不返回任何值,而且根本不打算返回。永不返回的函数总是会抛出错误或者运行一个无限循环(希望是有意为之的!)。

如果一个函数永远不会返回,添加一个显式的 : never 类型注解表示在调用该函数后的任何代码都不会执行。这个 fail 函数只会抛出错误,所以它可以帮助 TypeScript 的控制流分析将 param 缩小为 string 类型。

function fail(message: string): never {
    throw new Error(`Invariant failure: ${message}.`);
}

function workWithUnsafeParam(param: unknown) {
  if (typeof param !== "string") {
      fail(`param should be a string, not ${typeof param}`);
  }

  // Here, param is known to be type string
  param.toUpperCase(); // Ok
}
Never 不等同于 void。void 指没有返回值的函数。never 是指永不返回的函数。

函数重载

一些 JavaScript 函数可以使用截然不同的参数集来调用,这些参数集不能仅由可选和/或剩余参数表示。这些函数可以用称为重载签名的 TypeScript 语法来描述:在最后一个实现签名和函数体之前,多次声明函数名称、参数和返回类型的不同版本。

在确定是否为对重载功能的调用抛出语法错误时,TypeScript 将仅查看函数的重载签名。实现签名仅由函数的内部逻辑使用。

这个 createDate 函数旨在使用一个 timestamp 参数或三个参数 monthdayyear 调用。允许使用其中任一数量的参数进行调用,但使用两个参数进行调用会导致类型错误,因为没有重载签名允许两个参数。在此示例中,前两行是重载签名,第三行是实现签名:

function createDate(timestamp: number): Date;
function createDate(month: number, day: number, year: number): Date;
function createDate(monthOrTimestamp: number, day?: number, year?: number) {
  return day === undefined || year === undefined
    ? new Date(monthOrTimestamp)
    : new Date(year, monthOrTimestamp, day);
}

createDate(554356800); // Ok
createDate(7, 27, 1987); // Ok

createDate(4, 1);
// 错误:没有需要 2 参数的重载,
// 但存在需要 1 或 3 参数的重载。

签名重载与其他类型系统语法一样,在编译 TypeScript 输出 JavaScript 时会被删除。

前面的代码片段的函数将编译为大致如下的 JavaScript:

function createDate(monthOrTimestamp, day, year) {
  return day === undefined || year === undefined
    ? new Date(monthOrTimestamp)
    : new Date(year, monthOrTimestamp, day);
}
警告:函数重载通常用作复杂、难以描述的函数类型的最后手段。通常最好保持函数简单,并尽可能避免使用函数重载。

调用签名兼容性

重载函数实现中使用的实现签名是函数实现中参数类型和返回类型的签名。因此,函数重载签名中的返回类型和每个参数必须可分配给其实现签名中的相同索引的参数。换句话说,实现签名必须与所有重载签名兼容。

这个 format 函数的实现签名将它的第一个参数声明为 string。虽然前两个重载签名也兼容 string,但第三个重载签名的 () => string 类型是不兼容的:

function format(data: string): string; // Ok
function format(data: string, needle: string, haystack: string): string; // Ok

function format(getData: () => string): string;
// ~~~~~~~
// 此重载签名与其实现签名不兼容。

function format(data: string, needle?: string, haystack?: string) {
    return needle && haystack ? data.replace(needle, haystack) : data;
}

总结

在本章中,你学习了如何在 TypeScript 中推断或显式声明函数的参数和返回类型:

  • 使用类型注解声明函数的参数类型
  • 声明可选参数、默认值和剩余参数以改变类型系统的行为
  • 使用类型注解声明函数的返回类型
  • 使用 void 类型描述不返回可用值的函数
  • 使用 never 类型描述根本不返回的函数
  • 使用函数重载描述不同的函数调用签名
现在你已经读完了这一章,你最好练习一下学到的东西 https://learningtypescript.com/functions.

是什么让 TypeScript 项目变得更好?
它的功能表现良好。

0

评论 (0)

取消