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