Appearance
泛型函数
在写一个函数时,输入的类型与输出的类型有关,或者两个输入的类型以某种方式相关,这是常见的。
让我们考虑一下一个返回数组中第一个元素的函数。
typescript
function firstElement(arr: any[]) {
return arr[0];
}
这个函数完成了它的工作,但不幸的是它的返回类型是 any 。如果该函数返回数组元素的类型会更好。
在TypeScript中,当我们想描述两个值之间的对应关系时,会使用泛型。我们通过在函数签名中声明一个类型参数来做到这一点:
typescript
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
通过给这个函数添加一个类型参数 Type ,并在两个地方使用它,我们已经在函数的输入(数组)和输出(返回值)之间建立了一个联系。现在当我们调用它时,一个更具体的类型就出来了:
typescript
// s 是 'string' 类型
const s = firstElement(["a", "b", "c"]);
// n 是 'number' 类型
const n = firstElement([1, 2, 3]);
// u 是 undefined 类型
const u = firstElement([]);
请注意,在这个例子中,TypeScript可以推断出输入类型参数的类型(从给定的字符串数组),以及基于函数表达式的返回值(数字)的输出类型参数。
类型推断
请注意,在这个例子中,我们没有必要指定类型。类型是由TypeScript推断出来的--自动选择。
我们也可以使用多个类型参数。例如,一个独立版本的map看起来是这样的。
typescript
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可以推断出输入类型参数的类型(从给定的字符串数组),以及基于函数表达式的返回值(数字)的输出类型参数。
限制条件
我们已经写了一些通用函数,可以对任何类型的值进行操作。有时我们想把两个值联系起来,但只能对某个值的子集进行操作。在这种情况下,我们可以使用一个约束条件来限制一个类型参数可以接受的类型。 让我们写一个函数,返回两个值中较长的值。要做到这一点,我们需要一个长度属性,是一个数字。我们通过写一个扩展子句将类型参数限制在这个类型上。
typescript
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 属性。
使用受限值
这里有一个使用通用约束条件时的常见错误。
typescript
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,要么返回一个与该限制相匹配的值。
问题是,该函数承诺返回与传入的对象相同的类型,而不仅仅是与约束条件相匹配的一些对象。如果这段代码是合法的,你可以写出肯定无法工作的代码。
typescript
// 'arr' 获得值: { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
//在此崩溃,因为数组有一个'切片'方法,但没有返回对象!
console.log(arr.slice(0));
指定类型参数
TypeScript 通常可以推断出通用调用中的预期类型参数,但并非总是如此。例如,假设你写了一个函数来合并两个数组
typescript
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
通常情况下,用不匹配的数组调用这个函数是一个错误:
typescript
const arr = combine([1, 2, 3], ["hello"]);
然而,如果你打算这样做,你可以手动指定类型:
typescript
const arr = combine<string | number>([1, 2, 3], ["hello"]);
编写优秀通用函数的准则
编写泛型函数很有趣,而且很容易被类型参数所迷惑。有太多的类型参数或在不需要的地方使用约束,会使推理不那么成功,使你的函数的调用者感到沮丧。
- 类型参数下推 下面是两种看似相似的函数写法。
typescript
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] 表达式,而不是在调用期间 "等待 "解析该元素。
规则:在可能的情况下,使用类型参数本身,而不是对其进行约束
- 使用更少的类型参数 下面是另一对类似的函数。
typescript
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 除了使函数更难阅读和推理外,什么也没做。
规则:总是尽可能少地使用类型参数
- 类型参数应出现两次 有时我们会忘记,一个函数可能不需要是通用的:
typescript
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
我们完全可以写一个更简单的版本:
typescript
function greet(s: string) {
console.log("Hello, " + s);
}
记住,类型参数是用来关联多个值的类型的。如果一个类型参数在函数签名中只使用一次,那么它就没有任何关系。 规则:如果一个类型的参数只出现在一个地方,请重新考虑你是否真的需要它