函数
无论是本地函数,还是从其它模块导入的函数,或者是类上的方法,函数都是任何应用的基本组成部分。它们同样也是值,就和其它值一样,TypeScript 有很多种描述函数如何被调用的方式。接下来,让我们了解如何编写类型去描述函数吧。
函数类型表达式
最简单的描述函数的方式就是使用函数类型表达式。这些类型在语法上和箭头函数非常相似:
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
语法 (a: string) => void
表示“某个函数接受名为 a
的参数,类型为字符串,且该函数没有返回值”。和函数声明一样,如果没有指定参数类型,那么参数会被隐式推断为 any
类型。
注意参数名是必需的。函数类型
(string) => void
表示“某个函数接受名为string
的参数,类型为any
”
当然,我们也可以使用类型别名为某个函数类型命名:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
调用签名
在 JavaScript 中,函数可以拥有属性,以方便进行调用。但是,TypeScript 的函数类型表达式语法不允许声明属性。如果我们想要描述某个可以通过属性被调用的东西,那么我们可以在一个对象类型上编写一个调用签名:
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
注意这个语法和函数类型表达式的语法不太一样。在参数列表和返回值类型之间,它使用的是 :
而不是 =>
。
构造签名
JavaScript 函数也可以通过 new
运算符进行调用。TypeScript 将这种函数视为构造器,因为它们通常用于创建新对象。你可以在调用签名前面添加 new
关键字,从而编写一个构造签名:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
有些对象,比如 JavaScript 的 Date
对象,可以直接调用也可以通过 new
调用。你可以在同一类型中任意组合调用签名和构造签名:
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
泛型函数
我们经常需要编写某个函数,它的输入值类型和输出值类型相关联,或者两个输入值的类型在某种程度上相关联。假设现在有一个函数,它需要返回某个数组中的第一个元素:
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([]);
推断
注意在上面的例子中,我们不需要特殊说明 Type
的类型。因为 TypeScript 可以推断 —— 自动选择它的类型。
我们也可以使用多个类型参数。举个例子,可以像下面这样实现一个独立版本的 map
:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// n 的类型是 string
// parsed 的类型是 number[]
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
注意在这个例子中,TypeScript 可以基于给定的 string
类型数组推断出 Input
类型参数的类型,也可以基于函数表达式的返回值类型(number
)推断出 Output
类型参数的类型。
约束
我们目前编写的泛型函数适用于所有类型的值。有时候,我们想要关联两个值,但要求只能对值的某个子集进行操作。这时候,我们可以使用“约束”去限制类型参数可以接受的种类。
我们来编写一个函数,它可以返回两个值长度较长的那个。为了实现这个功能,我们需要一个 number
类型的 length
属性。这里,我们通过 extends
子句将类型参数约束为具有 length
属性的类型:
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");
// 报错!number 类型的值没有 length 属性
const notOK = longest(10, 100);
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
在这个例子中,没有什么有趣的事情值得注意。我们允许 TypeScript 推断 longest
函数返回值的类型。返回值的类型推断也适用于泛型函数。
我们将 Type
约束为 {length: number}
,因此得以访问 a
参数和 b
参数的 length
属性。如果没有类型约束,那么我们是无法访问这个属性的,因为传入的参数可能是其它不具备 length
属性的类型。
longerArray
和 longerString
的类型是基于函数参数推断出来的。记住,泛型都是将两个或多个值与同一类型相关联!
使用约束值
下面是使用泛型约束的时候常见的一个错误:
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
/*
Type '{ length: number; }' is not assignable to type 'Type'.
'{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
*/
}
}
这个函数看起来似乎没毛病 —— Type
被约束为 {length: number}
,函数要么返回 Type
,要么返回匹配约束条件的值。但问题在于,函数承诺返回一个与传入参数相同类型的对象,而不是某个匹配约束条件的对象。如果这段代码是合法的,那么你很可能写出下面这样无法正常运行的代码:
// 'arr' 的值是 { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// 这里会报错,因为 arr 不是数组,没有 slice 方法
console.log(arr.slice(0));
指定类型参数
在一次泛型调用中,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'.
不过,如果你本意就是想合并两个类型不匹配的数组,那么你可以手动指定 Type
:
const arr = combine<string | number>([1,2,3],["hello"]);
编写良好泛型函数的指南
编写泛型函数很有意思,并且很容易因为使用类型参数而忘乎所以。使用过多类型参数或者在不需要的时候使用约束条件,会导致类型推断很难成功,对函数的调用者造成困惑。
抑制类型参数
下面两种方式编写的函数很相似:
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
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");
网友评论