this
(高级)本章探讨 TypeScript 中函数的静态类型。
在本章中,“函数”指的是“函数、方法或构造函数”
在本章中,关于函数所说的大多数内容(尤其是参数处理方面)也适用于方法和构造函数。
以下是在 TypeScript 中的函数声明示例
function repeat1(str: string, times: number): string { // (A)
.repeat(times);
return str
}.equal(
assertrepeat1('*', 5), '*****');
参数:如果编译器选项 --noImplicitAny
处于启用状态(如果 --strict
处于启用状态,则该选项也会启用),则每个参数的类型必须是可推断的或显式指定的。(我们将在后面详细介绍类型推断。)在本例中,无法进行类型推断,因此 str
和 times
具有类型注释。
返回值:默认情况下,函数的返回类型是推断的。这通常已经足够好了。在本例中,我们选择显式指定 repeat1()
的返回类型为 string
(A 行中的最后一个类型注释)。
repeat1()
的箭头函数版本如下所示
= (str: string, times: number): string => {
const repeat2 .repeat(times);
return str; }
在这种情况下,我们也可以使用表达式体
= (str: string, times: number): string =>
const repeat3 .repeat(times); str
我们可以通过函数类型签名来定义函数的类型
type Repeat = (str: string, times: number) => string;
这种函数类型的名称是 Repeat
。 除其他外,它匹配所有具有以下特征的函数
string
和 number
。 我们需要在函数类型签名中命名参数,但在检查两个函数类型是否兼容时会忽略这些名称。string
。 请注意,这次类型是用箭头分隔的,并且不能省略。此类型匹配更多函数。 我们将在本章后面探讨赋值兼容性规则时,了解哪些函数与其匹配。
我们也可以使用接口来定义函数类型
interface Repeat {: string, times: number): string; // (A)
(str }
注意
一方面,接口更加冗长。 另一方面,它们允许我们指定函数的属性(这种情况很少见,但确实会发生)
interface Incrementor1 {: number): number;
(x: number;
increment }
我们也可以通过函数签名类型和对象字面量类型的交集类型 (&
) 来指定属性
type Incrementor2 =
: number) => number
(x& { increment: number }
;
例如,考虑以下情况:一个库导出以下函数类型。
type StringPredicate = (str: string) => boolean;
我们想定义一个类型与 StringPredicate
兼容的函数。 我们想立即检查是否确实如此(而不是在第一次使用它时才发现)。
如果我们使用 const
声明一个变量,我们可以通过类型注释来执行检查
: StringPredicate = (str) => str.length > 0; const pred1
请注意,我们不需要指定参数 str
的类型,因为 TypeScript 可以使用 StringPredicate
来推断它。
检查函数声明更加复杂
function pred2(str: string): boolean {
.length > 0;
return str
}
// Assign the function to a type-annotated variable
: StringPredicate = pred2; const pred2ImplementsStringPredicate
以下解决方案稍微有点过头了(也就是说,如果您不完全理解它,也不用担心),但它展示了一些高级功能
function pred3(...[str]: Parameters<StringPredicate>)
: ReturnType<StringPredicate> {
.length > 0;
return str }
参数:我们使用 Parameters<>
来提取一个包含参数类型的元组。 三个点声明了一个剩余参数,它将所有参数收集到一个元组/数组中。 [str]
对该元组进行解构。(本章稍后将详细介绍剩余参数。)
返回值:我们使用 ReturnType<>
来提取返回类型。
回顾:如果启用了 --noImplicitAny
(--strict
会启用它),则每个参数的类型必须是可推断的或显式指定的。
在以下示例中,TypeScript 无法推断 str
的类型,我们必须指定它
function twice(str: string) {
+ str;
return str }
在 A 行中,TypeScript 可以使用类型 StringMapFunction
来推断 str
的类型,我们不需要添加类型注释
type StringMapFunction = (str: string) => string;
: StringMapFunction = (str) => str + str; // (A) const twice
在这里,TypeScript 可以使用 .map()
的类型来推断 str
的类型
.deepEqual(
assert'a', 'b', 'c'].map((str) => str + str),
['aa', 'bb', 'cc']); [
这是 .map()
的类型
<T> {
interface Arraymap<U>(
: (value: T, index: number, array: T[]) => U,
callbackfn?: any
thisArg: U[];
)// ···
}
在本节中,我们将介绍几种允许省略参数的方法。
str?: string
如果我们在参数名称后面加上一个问号,则该参数将变为可选参数,并且可以在调用函数时省略
function trim1(str?: string): string {
// Internal type of str:
// %inferred-type: string | undefined
;
str
if (str === undefined) {
'';
return
}.trim();
return str
}
// External type of trim1:
// %inferred-type: (str?: string | undefined) => string
; trim1
以下是调用 trim1()
的方式
.equal(
asserttrim1('\n abc \t'), 'abc');
.equal(
asserttrim1(), '');
// `undefined` is equivalent to omitting the parameter
.equal(
asserttrim1(undefined), '');
str: undefined|string
在外部,trim1()
的参数 str
的类型为 string|undefined
。 因此,trim1()
在大多数情况下等效于以下函数。
function trim2(str: undefined|string): string {
// Internal type of str:
// %inferred-type: string | undefined
;
str
if (str === undefined) {
'';
return
}.trim();
return str
}
// External type of trim2:
// %inferred-type: (str: string | undefined) => string
; trim2
trim2()
与 trim1()
的唯一区别是,在函数调用中不能省略该参数(A 行)。 换句话说:当省略类型为 undefined|T
的参数时,我们必须明确说明。
.equal(
asserttrim2('\n abc \t'), 'abc');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
trim2(); // (A)
.equal(
asserttrim2(undefined), ''); // OK!
str = ''
如果我们为 str
指定了参数默认值,则不需要提供类型注释,因为 TypeScript 可以推断出类型
function trim3(str = ''): string {
// Internal type of str:
// %inferred-type: string
;
str
.trim();
return str
}
// External type of trim2:
// %inferred-type: (str?: string) => string
; trim3
请注意,str
的内部类型为 string
,因为默认值确保它永远不会是 undefined
。
让我们调用 trim3()
.equal(
asserttrim3('\n abc \t'), 'abc');
// Omitting the parameter triggers the parameter default value:
.equal(
asserttrim3(), '');
// `undefined` is allowed and triggers the parameter default value:
.equal(
asserttrim3(undefined), '');
我们也可以同时指定类型和默认值
function trim4(str: string = ''): string {
.trim();
return str }
剩余参数将所有剩余参数收集到一个数组中。 因此,它的静态类型通常是一个数组。 在以下示例中,parts
是一个剩余参数
function join(separator: string, ...parts: string[]) {
.join(separator);
return parts
}.equal(
assertjoin('-', 'state', 'of', 'the', 'art'),
'state-of-the-art');
下一个示例演示了两个功能
[string, number]
。function repeat1(...[str, times]: [string, number]): string {
.repeat(times);
return str }
repeat1()
等效于以下函数
function repeat2(str: string, times: number): string {
.repeat(times);
return str }
命名参数 是 JavaScript 中一种流行的模式,其中使用对象字面量为每个参数指定名称。 如下所示
.equal(
assertpadStart({str: '7', len: 3, fillStr: '0'}),
'007');
在纯 JavaScript 中,函数可以使用解构来访问命名参数值。 但是,在 TypeScript 中,我们还必须为对象字面量指定一个类型,这会导致冗余
function padStart({ str, len, fillStr = ' ' } // (A)
: { str: string, len: number, fillStr: string }) { // (B)
.padStart(len, fillStr);
return str }
请注意,解构(包括 fillStr
的默认值)都发生在 A 行中,而 B 行专门用于 TypeScript。
可以使用单独的类型来代替我们在 B 行中使用的内联对象字面量类型。 但是,在大多数情况下,我更喜欢不这样做,因为它稍微违背了参数的本质,即每个函数的参数都是局部且唯一的。 如果你喜欢函数头部的东西少一些,那也没关系。
this
(高级)每个普通函数始终都有一个隐式参数 this
,这使得它可以在对象中用作方法。 有时我们需要为 this
指定一个类型。 TypeScript 为此用例提供了专用语法:普通函数的参数之一可以命名为 this
。 这样的参数只在编译时存在,在运行时消失。
例如,请考虑以下 DOM 事件源的接口(略微简化版本)
interface EventSource {addEventListener(
: string,
type: (this: EventSource, ev: Event) => any,
listener?: boolean | AddEventListenerOptions
options: void;
)// ···
}
回调函数 listener
的 this
始终是 EventSource
的实例。
下一个示例演示了 TypeScript 如何使用 this
参数提供的类型信息来检查 .call()
的第一个参数(A 行和 B 行)
function toIsoString(this: Date): string {
.toISOString();
return this
}
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'Date'. (2345)
.throws(() => toIsoString.call('abc')); // (A) error
assert
.call(new Date()); // (B) OK toIsoString
此外,我们不能将 toIsoString()
作为对象 obj
的方法调用,因为那样它的接收者就不是 Date
的实例
= { toIsoString };
const obj // @ts-expect-error: The 'this' context of type
// '{ toIsoString: (this: Date) => string; }' is not assignable to
// method's 'this' of type 'Date'. [...]
.throws(() => obj.toIsoString()); // error
assert.toIsoString.call(new Date()); // OK obj
有时,单个类型签名不足以描述函数的工作方式。
考虑函数 getFullName()
,我们在以下示例中调用它(A 行和 B 行)
interface Customer {: string;
id: string;
fullName
}= {id: '1234', fullName: 'Jane Bond'};
const jane = {id: '5678', fullName: 'Lars Croft'};
const lars = new Map<string, Customer>([
const idToCustomer '1234', jane],
['5678', lars],
[;
])
.equal(
assertgetFullName(idToCustomer, '1234'), 'Jane Bond'); // (A)
.equal(
assertgetFullName(lars), 'Lars Croft'); // (B)
我们如何实现 getFullName()
? 以下实现适用于上一个示例中的两个函数调用
function getFullName(
: Customer | Map<string, Customer>,
customerOrMap?: string
id: string {
)if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
= customerOrMap.get(id);
const customer if (customer === undefined) {
new Error('Unknown ID: ' + id);
throw
}= customer;
customerOrMap
} else {if (id !== undefined) throw new Error();
}.fullName;
return customerOrMap }
但是,使用此类型签名,在编译时合法的函数调用会在运行时产生错误
.throws(() => getFullName(idToCustomer)); // missing ID
assert.throws(() => getFullName(lars, '5678')); // ID not allowed assert
以下代码修复了这些问题
function getFullName(customerOrMap: Customer): string; // (A)
function getFullName( // (B)
: Map<string, Customer>, id: string): string;
customerOrMapfunction getFullName( // (C)
: Customer | Map<string, Customer>,
customerOrMap?: string
id: string {
)// ···
}
// @ts-expect-error: Argument of type 'Map<string, Customer>' is not
// assignable to parameter of type 'Customer'. [...]
getFullName(idToCustomer); // missing ID
// @ts-expect-error: Argument of type '{ id: string; fullName: string; }'
// is not assignable to parameter of type 'Map<string, Customer>'.
// [...]
getFullName(lars, '5678'); // ID not allowed
这里发生了什么? getFullName()
的类型签名被重载了
getFullName()
。 实际实现的类型签名不能使用!我的建议是,只有在无法避免的情况下才使用重载。 一种替代方法是将重载函数拆分为多个具有不同名称的函数,例如
getFullName()
getFullNameViaMap()
在接口中,我们可以有多个不同的调用签名。 这使我们能够在以下示例中使用接口 GetFullName
进行重载
interface GetFullName {: Customer): string;
(customerOrMap: Map<string, Customer>, id: string): string;
(customerOrMap
}
: GetFullName = (
const getFullName: Customer | Map<string, Customer>,
customerOrMap?: string
id: string => {
)if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
= customerOrMap.get(id);
const customer if (customer === undefined) {
new Error('Unknown ID: ' + id);
throw
}= customer;
customerOrMap
} else {if (id !== undefined) throw new Error();
}.fullName;
return customerOrMap }
在下一个示例中,我们重载并使用字符串字面量类型(例如 'click'
)。 这允许我们根据参数 type
的值更改参数 listener
的类型
function addEventListener(elem: HTMLElement, type: 'click',
: (event: MouseEvent) => void): void;
listenerfunction addEventListener(elem: HTMLElement, type: 'keypress',
: (event: KeyboardEvent) => void): void;
listenerfunction addEventListener(elem: HTMLElement, type: string, // (A)
: (event: any) => void): void {
listener.addEventListener(type, listener); // (B)
elem }
在这种情况下,要正确获取实现的类型(从 A 行开始)相对困难,以便主体中的语句(B 行)能够正常工作。 作为最后的手段,我们始终可以使用类型 any
。
下一个示例演示了方法的重载:方法 .add()
被重载。
class StringBuilder {= '';
#data
add(num: number): this;
add(bool: boolean): this;
add(str: string): this;
add(value: any): this {
.#data += String(value);
this;
return this
}
toString() {
.#data;
return this
}
}
= new StringBuilder();
const sb
sb.add('I can see ')
.add(3)
.add(' monkeys!')
;
.equal(
assert.toString(), 'I can see 3 monkeys!') sb
Array.from()
的类型定义是重载接口方法的一个示例
interface ArrayConstructor {from<T>(arrayLike: ArrayLike<T>): T[];
from<T, U>(
: ArrayLike<T>,
arrayLike: (v: T, k: number) => U,
mapfn?: any
thisArg: U[];
) }
在第一个签名中,返回的数组与其参数具有相同的元素类型。
在第二个签名中,返回的数组的元素与 mapfn
的结果具有相同的类型。 此版本的 Array.from()
类似于 Array.prototype.map()
。
在本节中,我们将研究可赋值性的类型兼容性规则:Src
类型的函数是否可以传输到 Trg
类型的存储位置(变量、对象属性、参数等)?
了解可赋值性有助于我们回答以下问题,例如
在本小节中,我们将研究可赋值性的一般规则(包括函数的规则)。在下一小节中,我们将探讨这些规则对函数的意义。
如果满足以下条件之一,则类型 Src
可赋值 给类型 Trg
Src
和 Trg
是相同的类型。Src
或 Trg
是 any
类型。Src
是字符串字面量类型,而 Trg
是原始类型 String。Src
是联合类型,并且 Src
的每个组成类型都可赋值给 Trg
。Src
和 Trg
是函数类型,并且Trg
具有剩余参数,或者 Src
的必需参数数量小于或等于 Trg
的参数总数。Trg
中的每个参数类型都可赋值给 Src
中的相应参数类型。Trg
的返回类型是 void
,或者 Src
的返回类型可赋值给 Trg
的返回类型。在本小节中,我们将了解赋值规则对以下两个函数 targetFunc
和 sourceFunc
的意义
: Trg = sourceFunc; const targetFunc
示例
: (x: RegExp) => Object = (x: Object) => /abc/; const trg1
以下示例演示了如果目标返回类型是 void
,则源返回类型无关紧要。为什么呢?在 TypeScript 中,void
结果总是被忽略。
: () => void = () => new Date(); const trg2
源的参数数量不得超过目标
// @ts-expect-error: Type '(x: string) => string' is not assignable to
// type '() => string'. (2322)
: () => string = (x: string) => 'abc'; const trg3
源的参数数量可以少于目标
: (x: string) => string = () => 'abc'; const trg4
为什么呢?目标指定了对源的期望:它必须接受参数 x
。它确实如此(但它忽略了它)。这种允许性使得
'a', 'b'].map(x => x + x) [
.map()
的回调函数只有 .map()
类型签名中提到的三个参数中的一个
map<U>(
: (value: T, index: number, array: T[]) => U,
callback?: any
thisArg: U[]; )