1、编写良好泛型函数的准则
编写泛型函数很有趣,并且很容易被类型参数冲昏头脑。 类型参数过多或在不需要的地方使用约束会使推理不太成功,从而使函数的调用方感到沮丧。
1.1 向下推送类型参数
以下是编写看起来相似的函数的两种方法:
function catArr<Type>(params: Type[]) { return params; } function dogArr<Type extends any[]>(params: Type){ return params; } catArr(['波斯猫', '橘猫', '拿破仑']) dogArr(['哈士奇', '金毛', '博美'])
乍一看,这些可能看起来完全相同,但catArr是编写此函数的更好方法。其推断的返回类型为type,但dogArr的推断返回类型为any,因为TypeScript必须使用约束类型解析arr[0]表达式,而不是在调用期间“等待”解析元素。
注意:如果可能,请使用类型参数本身,而不是约束它。
1.2 使用较少的类型参数
参考上面的代码,我们改写一下:
function catArr<Type>(params: Type[], func: (arg: Type) => Type): Type[] { return params.filter(func); } function dogArr<Type, Func extends (arg: Type) => Type> (params: Type[], func: Func): Type[] { return params.filter(func); }
我们已经创建了一个类型参数Func,它与两个值不相关。这总是一个危险信号,因为这意味着想要指定类型参数的调用方必须毫无理由地手动指定一个额外的类型参数。Func什么也没做,只是让这个函数更难阅读和推理!
注意:始终使用尽可能少的类型参数
1.3 类型参数应出现两次
有时我们忘记了一个函数可能不需要是泛型的:
function cat<Name extends string>(N: Name): string { console.log('猫的名字是:: ' + N ); return N; } cat('拿破仑') // 猫的名字是:: 拿破仑
我们可以很容易地编写一个更简单的版本:
function cat(N: string): string { console.log('猫的名字是:: ' + N ); return N; } cat('拿破仑') // 猫的名字是:: 拿破仑
请记住,类型参数用于关联多个值的类型。如果一个类型参数在函数签名中只使用过一次,那么它与任何内容都没有关联。这包括推断的返回类型;例如,如果Name是cat的推断返回类型的一部分,那么它将关联参数和返回类型,因此尽管在编写的代码中只出现一次,但仍将使用两次。
注意:如果类型参数只出现在一个位置,请重新考虑是否确实需要它
2、可选参数
JavaScript 中的函数通常采用可变数量的参数。例如数字转2,8,16进制。
function N(n: number) { console.log(n.toString()); // 17 console.log(n.toString(2)); // 0001 console.log(n.toString(8)); // 21 console.log(n.toString(16)); // 11 } N(17)
我们可以在 TypeScript 中对此方法进行改造,将参数标记为可选:?
function N(x:number, n?: number) { console.log(x.toString(n)); // 17 } N(17)
还可以提供参数默认值,例如不传进制参数默认为十六进制:
function N(x:number, n = 16) { console.log(x.toString(n)); // 11 } N(17)
现在在一下内容中f函数,n 是一个数字类型,因为任何undefined参数都会被替换成10,注意,当参数是可选的时候,调用方总是可以传递未定义的参数,因为这只是模拟一个“错误”的参数:
function N(x : number, n? : number) { console.log(x.toString(n)); // 11 } N(17) // 17 N(17, 10) // 17 N(17, undefined) // 17
3、回调中的可选参数
了解可选参数和函数类型表达式后,在编写调用回调的函数时很容易犯以下错误:
function catArr(arr: any[], callback: (arg: any, index?: number) => void){ let count = arr.length; for(let i =0 ; i< count; i++){ callback(arr[i], i) } } catArr(['波斯猫', '橘猫', '拿破仑'], (v) =>{ console.log(v); }) catArr(['波斯猫', '橘猫', '拿破仑'], (v, i) =>{ console.log(v, i); })
如果在回调函数的可选参数,调用原型方法时,可以会发生错误。
catArr(['波斯猫', '橘猫', '拿破仑'], (v, i) =>{ console.log(v, i.toString()); // “i”可能为“未定义”。 })
在 JavaScript 中,如果调用的函数的参数多于参数,则忽略额外的参数。 TypeScript 的行为方式相同。 具有较少参数(相同类型)的函数始终可以取代具有更多参数的函数。
注意:为回调编写函数类型时,切勿编写可选参数,除非您打算在不传递该参数的情况下调用对应的方法。
4、函数重载
一些JavaScript函数可以在各种参数计数和类型中调用。例如,您可以编写一个函数来生成日期,该日期采用时间戳(一个参数)或月/日/年规范(三个参数)。
在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); // 没有需要 2 参数的重载,但存在需要 1 或 3 参数的重载。
在这个例子中,我们写了两个重载:一个接受一个自变量,另一个接受三个自变量。前两个签名称为过载签名。
然后,我们编写了一个具有兼容签名的函数实现。函数有一个实现签名,但不能直接调用此签名。尽管我们在所需参数之后编写了一个带有两个可选参数的函数,但它不能用两个参数调用!
5、重载签名和实现签名
这是造成混淆的常见来源。 通常人们会写这样的代码,而不明白为什么会有错误:
function fn(x: string): void; function fn() { // ... } fn(); // 应有 1 个参数,但获得 0 个。
同样,用于编写函数体的签名无法从外部“看到”。
从外部看不到实现的签名。在编写重载函数时,应该始终在函数的实现之上有两个或多个签名。
实现签名还必须与重载签名兼容。 例如,这些函数存在错误,因为实现签名与重载不匹配:
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"; }
6、写好重载
与泛型一样,在使用函数重载时应遵循一些准则。 遵循这些原则将使函数更易于调用、更易于理解和更易于实现。
让我们考虑一个返回字符串或数组长度的函数:
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]); // 没有与此调用匹配的重载。 // 第 1 个重载(共 2 个),“(s: string): number”,出现以下错误。 // 类型“number[] | "hello"”的参数不能赋给类型“string”的参数。 // 不能将类型“number[]”分配给类型“string”。 // 第 2 个重载(共 2 个),“(arr: any[]): number”,出现以下错误。 // 类型“number[] | "hello"”的参数不能赋给类型“any[]”的参数。 // 不能将类型“string”分配给类型“any[]”。
由于两个重载具有相同的参数计数和相同的返回类型,因此我们可以改为编写函数的非重载版本:
function len(x: any[] | string) { return x.length; }
这要好得多! 调用方可以使用任何一种值来调用它,作为额外的好处,我们不必找出正确的实现签名。
注意:尽可能首选具有联合类型的参数,而不是重载。