TypeScript学习文档三------函数
五 函数更多
函数是任何应用程序的基本构件,无论它们是本地函数,从另一个模块导入,还是一个类上的方法。它们也是值,就像其他值一样,TypeScript有很多方法来描述如何调用函数。让我们来学习一下如何编写描述函数的类型。
5.1 函数类型表达式
描述一个函数的最简单是用一个函数类型表达式. 这些类型在语法上类似于箭头函数.
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
语法 (a: string) => void
意味着有一个参数的函数,名为 a
,类型为字符串,没有返回值"。就像函数声明一样,如果没有指定参数类型,它就隐含为 any
类型。
当然, 我们可以用一个类型别名来命名一个函数类型.
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
5.2 调用签名: 属性签名
在JavaScript中,除了可调用之外,函数还可以有属性。然而,函数类型表达式的语法不允许声明属性。 如果我们想用属性来描述可调用的东西,我们可以在一个类型别名中写一个调用签名。
值得注意的是, 类型别名中缩写的函数类型表达式返回值是用冒号:而不是箭头函数=>, 且实际应用时所传入函数返回值必须与此函数类型表达式声明的相同(fn1和fn2), 如果函数体内没有操作参数的行为, 可以不传参数(比如fn3).
type DescribableFunction = { // 对象类型
description: string // 函数的属性签名
(someArg: number): boolean // 函数类型表达式, 不能用=> 而是用:
}
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6))
}
// 传入正常参数使用且正常返回值
function fn1(n: number) {
console.log(n)
return true
}
fn1.description = "hello"
// 不传入参数且不正常返回值
function fn2() {
console.log("lalala")
}
fn2.description = "heihei"
// 不传入参数且正常返回值
function fn3() {
return false
}
fn3.description = "hehehe"
doSomething(fn1) // 正常
doSomething(fn2) // 报错
doSomething(fn3) // 正常
5.3 构造签名
JS函数也可以用new
操作符来调用. TS将这些成为构造函数, 因为它们通常会创建一个新的对象。你可以通过在调用签名前面添加 new
关键字来写一个构造签名, 返回的是一个类或者构造函数.
class Ctor {
s: string
constructor(s: string) {
this.s = s
}
}
type SomeConstructor = { // 在调用签名前加new就是构造签名
new (s: string): Ctor // 返回的是一个构造函数或者类
}
function fn(ctor: SomeConstructor) { // SomeConstructor可以理解为构造函数
return new ctor("hello")
}
const f = fn(Ctor)
console.log(f.s)
有些对象,如 JavaScript 的 Date
对象,可以在有 new
或没有 new
的情况下被调用。你可以在同一类型中任意地结合调用和构造签名.
interface CallOrConstruct {
new (s: string): Date
(): string
}
function fn(date: CallOrConstruct) {
let d = new date('2021-11-20')
let n = date() // 因为Date可以在不使用new的情况下调用所以代码正常
console.log(d)
console.log(n)
}
fn(Date)
下一个实例
// clock构造函数的接口, 是一个构造签名, 返回一个ClockInterface类的构造函数
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
// Clock类的接口, 里面有一个tick()函数
interface ClockInterface {
tick(): void;
}
// 创建Clock类的函数
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
// 具体的类来实现ClockInterface, 必须要有tick函数
class DigitalClock implements ClockInterface {
h: number
m: number
constructor(h: number, m: number) {
this.h = h
this.m = m
}
tick() {
console.log("beep beep");
}
}
// 具体的类来实现ClockInterface, 必须要有tick函数
class AnalogClock implements ClockInterface {
h: number
m: number
constructor(h: number, m: number) {
this.h = h
this.m = m
}
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
console.log(digital)
analog.tick()
5.4 泛型函数
在写一个函数时, 输入的类型与输出的类型有关, 或者两个输入的类型以某种方式相关, 这是常见的. 让我们考虑一下一个返回数组种第一个元素的函数.
function firstElement(arr: any[]) {
return arr[0]
}
这个函数完成了它的工作,但不幸的是它的返回类型是 any
。如果该函数返回数组元素的类型会更好。
在TypeScript中,当我们想描述两个值之间的对应关系时,会使用泛型。我们通过在函数签名中声明一个类型参数来做到这一点:
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0]
}
通过给这个函数添加一个类型参数 Type
,并在两个地方使用它,我们已经在函数的输入(数组)和输出(返回值)之间建立了一个联系。现在当我们调用它时,一个更具体的类型就出来了:
// s 是 'string' 类型
const s = firstElement(["a", "b", "c"]);
// n 是 'number' 类型
const n = firstElement([1, 2, 3]);
// u 是 undefined 类型
const u = firstElement([]);
5.4.1 类型推断
请注意, 在这个例子中, 我们没有必要指定类型. 类型是由TS推断出来的------自动选择.
我们也可以使用多个类型参数. 例如, 一个独立版本的map看起来可能是这样的:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func)
}
// 参数'n'是'字符串'类型。
// 'parsed'是'number[]'类型。
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
请注意,在这个例子中,TypeScript可以推断出输入类型参数的类型(从给定的字符串数组string),以及基于函数表达式的返回值(数字number)的输出类型参数。
5.4.2 限制条件
我们i经写了一些通用函数, 可以对任何类型的值进行操作. 有时我们想把两个值联系起来, 但只能对某个值的子集进行操作. 这种在这种情况下,我们可以使用一个约束条件来限制一个类型参数可以接受的类型。
让我们写一个函数,返回两个值中较长的值。要做到这一点,我们需要一个长度属性,是一个数字。我们通过写一个扩展子句将类型参数限制在这个类型上.
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");
// 错误! 数字没有'长度'属性
const notOK = longest(10, 100);
在这个例子中,有一些有趣的事情需要注意。我们允许TypeScript推断 longest
的返回类型。返回类型推断也适用于通用函数。
因为我们将 Type 约束为 { length: number }
,所以我们被允许访问 a 和 b 参数的 .length
属 性。如果没有类型约束,我们就不能访问这些属性,因为这些值可能是一些没有长度属性的其他类型。
longerArray 和 longerString 的类型是根据参数推断出来的。记住,泛型就是把两个或多个具有相同类型的值联系起来。 最后,正如我们所希望的,对 longest(10, 100) 的调用被拒绝了,因为数字类型没有一个 .length
属性
5.4.3 使用受限值
这里有一个使用通用约束条件时的常见错误。
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,要么返回一 个与该限制相匹配的值。问题是,该函数承诺返回与传入的对象相同的类型,而不仅仅是与约束条件相匹配的一些对象。如果这段代码是合法的,你可以写出肯定无法工作的代码。
// 'arr' 获得值: { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
//在此崩溃,因为数组有一个'切片'方法,但没有返回对象!
console.log(arr.slice(0));
5.4.4 指定类型参数
TypeScript 通常可以推断出通用调用中的预期类型参数,但并非总是如此。例如,假设你写了一个函数来合并两个数组:
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2)
}
通常情况下,用不匹配的数组调用这个函数是一个错误:
const arr = combine([1, 2, 3], ["hello"]);
然而,如果你打算这样做,你在调用函数时可以手动指定类型:
const arr = combine<string | number>([1, 2, 3], ["hello"])
5.4.5 编写优秀通用函数的准则
编写泛型函数很有趣,而且很容易被类型参数所迷惑。有太多的类型参数或在不需要的地方使用约束,会使推理不那么成功,使你的函数的调用者感到沮丧。
- 类型参数下推
下面是两种看似的函数写法:
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]);
乍一看,这些可能是相同的,但 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);
}
我们已经创建了一个类型参数 Func
,它并不涉及两个值。这总是一个值得标记的坏习惯,因为它意味 着想要指定类型参数的调用者必须无缘无故地手动指定一个额外的类型参数。 Func 除了使函数更难阅 读和推理外,什么也没做。
规则: 总是尽可能少的使用类型参数
- 类型参数应该出现两次及以上
有时候我们会忘记, 一个函数可能不需要是通用的:
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
我们完全可以写一个更简单的版本:
function greet(s: string) {
console.log("Hello, " + s);
}
记住,类型参数是用来关联多个值的类型的。如果一个类型参数在函数签名中只使用一次,那么它就没有任何关系。
规则: 如果一个类型的参数只出现在一个地方, 请重新考虑你是否真的需要它
5.5 可选参数 ?
JavaScript中的函数经常需要一个可变数量的参数。例如,number
的 toFixed
方法需要一个可选的数字计数。
function f(n: number) {
console.log(n.toFixed()); // 0 个参数
console.log(n.toFixed(3)); // 1 个参数
}
我们可以在TypeScript中通过将参数用 ?
标记:
function f(x?: number) {
// ...
}
f(); // 正确
f(10); // 正确
虽然参数被指定为 number 类型,但 x
参数实际上将具有 number | undefined
类型,因为在 JavaScript中未指定的参数会得到 undefined
的值。
你也可以提供一个参数默认值
function f(x = 10) {
//...
}
现在在 f
的主体中, x 将具有 number
类型,因为任何 undefined
的参数将被替换为10 。请注意,当一个参数是可选的,调用者总是可以传递未定义的参数,因为这只是模拟一个 "丢失 "的参数:
declare function f(x?: number): void;
// 以下调用都是正确的
f();
f(10);
f(undefined);
5.5.1 回调中的可选参数
一旦你了解了可选参数和函数类型表达式, 在编写调用回调的函数时就很容易犯以下错误:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
我们在写index?
作为一个可选参数时, 通常是想让这些调用都是合法的:
myForEach([1, 2, 3], (a) => console.log(a))
myForEach([1, 2, 3], (a, i) => console.log(a, i))
这实际上意味着回调可能会被调用,只有一个参数。换句话说,该函数定义说,实现可能是这样的:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// 我现在不想提供索引
callback(arr[i]);
}
}
反过来,TypeScript会强制执行这个意思,并发出实际上不可能的错误:
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed())
})
在JavaScript中,如果你调用一个形参多于实参的函数,额外的参数会被简单地忽略。TypeScript的行为也是如此。
参数较少的函数(相同的类型)总是可以取代参数较多的函数的位置。
当为回调写一个函数类型时, 永远不要写一个可选参数, 除非你打算在不传递该参数的情况下调用函数.
5.6 函数重载: 重载签名
一些 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)
在这个例子中,我们写了两个重载:一个接受一个参数,另一个接受三个参数。这前两个签名被称为重载签名。
然后,我们写了一个具有兼容签名的函数实现。函数有一个实现签名,但这个签名不能被直接调用。即使我们写了一个在所需参数之后有两个可选参数的函数,它也不能以两个参数被调用
5.6.1 重载签名和实现签名
这是一个常简的混乱的来源. 通常我们会写这样的代码, 却不明白为什么会出现错误:
function fn(x: string): void
function fn() {
// ...
}
// 期望能够以零参调用
fn()
同样, 用于编写函数体的签名不能从外面"看到":
实现的签名从外面是看不到的. 在编写重载函数时, 你应该总是在函数的实现上面有两个或多个签名.
实现签名也必须与重载签名兼容. 例如, 这些函数有错误, 因为实现签名没有以正确的方式匹配重载:
function fn(x: boolean): void;
// 参数类型不正确
function fn(x: string): void;
function fn(x: boolean) {}
function fn(x: string): string
// 返回类型不正确
function fn(x: number): boolean
function fn(x: string | number) {
return "oops";
}
5.6.2 编写好的重载
和泛型一样,在使用函数重载时,有一些准则是你应该遵循的。遵循这些原则将使你的函数更容易调用,更容易理解,更容易实现。
让我们考虑一个返回字符串或数组长度的函数:
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
这个函数是好的;我们可以用字符串或数组来调用它。然而,我们不能用一个可能是字符串或数组的值来调用它,因为TypeScript只能将一个函数调用解析为一个重载:
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
因为两个重载都有相同的参数数量和相同的返回类型,我们可以改写一个非重载版本的函数:
function len(x: any[] | string) {
return x.length;
}
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); // OK
这就好得多了! 调用者可以用任何一种值来调用它,而且作为额外的奖励,我们不需要找出一个正确的实现签名。
在可能的情况下,总是倾向于使用联合类型的参数而不是重载参数
5.6.3 函数内This
的声明
TS会通过代码分析来推断函数中this
应该是什么, 比如下面的例子:
const user = {
id: 123,
admin: false,
becomeAdmin: function () {
this.admin = true;
},
};
TS理解函数user.becomeAdmin
有一个对应的this
, 它是外部对象user
. 这个对于很多情况来说已经足够了, 但是有很多情况下你需要更多的控制this
代表什么对象/
JavaScript规范规定, 你不能有一个叫 this
的参数,所以TypeScript使用这个语法空间,让你在函数体中声明 this
的类型。
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
console.log(deck.createCardPicker()())
5.7 需要了解的其他类型
有一些额外的类型你会想要认识,它们在处理函数类型时经常出现。像所有的类型一样,你可以在任何地方使用它们,但这些类型在函数的上下文中特别相关.
5.7.1 void
void
表示没有返回值的函数的返回值. 当一个函数没有任何返回语句, 或者没有从这些返回语句中返回任何明确的值时, 它都是推断出来void
类型.
// 推断出的返回类型是void
function noop() {
return;
}
在JavaScript中,一个不返回任何值的函数将隐含地返回 undefinded
的值。然而,在TypeScript中, void 和 undefined
是不一样的。在本章末尾有进一步的细节。
void 和 undefined是不一样的
5.7.2 object
特殊类型object
指的是任何不是基元的值( string
、 number
、 bigint
、 boolean
、 symbol
、 null
或 undefined
)。这与空对象类型 { } 不同,也与全局类型 Object
不同。你很可能永远不会使用 Object
。
object
不是Object
。始终使用object
!
请注意,在JavaScript中,函数值是对象。它们有属性,在它们的原型链中有 Object.prototype ,是 Object 的实例,你可以对它们调用 Object.key ,等等。由于这个原因,函数类型在TypeScript中被 认为是 object 。
5.7.3 unknown
unknown
类型代表任何值. 这与any
类型相似, 但更安全, 因为对未知unknown
值做任何事情都是不合法的.
function f1(a: any) {
a.b(); // 正确
}
function f2(a: unknown) {
a.b();
}
这在描述函数类型时很有用,因为你可以描述接受任何值的函数,而不需要在函数体中有 any 值。 反之,你可以描述一个返回未知类型的值的函数:
function safeParse(s: string): unknown {
return JSON.parse(s);
}
// 需要小心对待'obj'!
const obj = safeParse(someRandomString);
5.7.4 never
有些函数永远不会返回一个值:
function fail(msg: string): never {
throw new Error(msg);
}
never
类型标识永远不会被观察到的值. 载一个返回类型中, 这意味着函数抛出了一个异常或终止程序的执行.
never 也出现在TypeScript确定一个 union
中没有任何东西的时候。
function fn(x: string | number) {
if (typeof x === "string") {
// 做一些事
} else if (typeof x === "number") {
// 再做一些事
} else {
x; // 'never'!
}
5.7.5 Function
全局性的 Function
类型描述了诸如 bind
、 call
、 apply
和其他存在于JavaScript中所有函数值的属性。它还有一个特殊的属性,即 Function
类型的值总是可以被调用;这些调用返回 any
。
function doSomething(f: Function) {
return f(1, 2, 3);
}
这是一个无类型的函数调用,一般来说最好避免,因为 any 返回类型都不安全。 如果你需要接受一个任意的函数,但不打算调用它,一般来说, () => void
的类型比较安全。
5.8 函数展开运算符
5.8.1 形参展开(Rest Parameters)
除了使用可选参数或重载来制作可以接受各种固定参数数量的函数之外,我们还可以使用休止参数来定义接受无限制数量的参数的函数。
rest
参数出现在所有其他参数之后,并使用 ...
的语法:
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);
在TypeScript中,这些参数的类型注解是隐含的 any[]
,而不是 any
,任何给出的类型注解必须是 Array
或 T[]
的形式,或一个元组类型(我们将在后面学习).
5.8.2 实参展开(Rest Arguments)
反之, 我们可以使用spread
语法从数组中提供可变数量的参数. 例如数组的push
方法需要任意数量的参数.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
console.log(arr1) //[ 1, 2, 3, 4, 5, 6 ]
请注意,一般来说,TypeScript并不假定数组是不可变的。这可能会导致一些令人惊讶的行为。
// 推断的类型是 number[] -- "一个有零或多个数字的数组"。
// 不专指两个数字
const args = [8, 5];
const angle = Math.atan2(...args);
这种情况的最佳解决方案取决于你的代码,但一般来说, const context
是最直接的解决方案
// 推断为2个长度的元组
const args = [8, 5] as const;
// 正确
const angle = Math.atan2(...args);
5.9 参数解构
可以使用参数重构来方便地将作为参数提供的对象,解压到函数主体的一个或多个局部变量中。在 JavaScript中,它看起来像这样:
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);
}
5.10 函数的可分配性: 返回void
类型
函数的 void
返回类型可以产生一些不寻常的,但却是预期的行为。
返回类型为 void
的上下文类型并不强迫函数不返回东西。另一种说法是,一个具有 void
返回类型的上下文函数类型( type vf = () => void
),在实现时,可以返回任何其他的值,但它会被忽略。
因此,以下 () => void
类型的实现是有效的:
type voidFunc = () => void
const f1: voidFunc = () => {
return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
return true
}
而当这些函数之一的返回值被分配给另一个变量时,它将保留 void
的类型
const v1 = f1();
const v2 = f2();
const v3 = f3();
console.log(v1) // true
console.log(v2) // true
console.log(v3) // true
这种行为的存在使得下面的代码是有效的,即使 Array.prototype.push
返回一个数字,而Array.prototype.forEach
方法期望一个返回类型为 void
的函数:
const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));
还有一个需要注意的特殊情况,当一个字面的函数定义有一个 void
的返回类型时,该函数必须不返回任何东西。
function f2(): void {
return true;
}
const f3 = function (): void {
return true;
}