我们经常需要写这种函数,即函数的输出类型依赖函数的输入类型,或者两个输入的类型以某种形式相互关联。让我们考虑这样一个函数,它返回数组的第一个元素:
function firstElement(arr: any[]) {
return arr[0];
}
注意此时函数返回值的类型是 any,如果能返回第一个元素的具体类型就更好了。
此时我们需要在函数签名里声明一个类型参数 (type parameter):
function firstElement(arr: T[]): T {
return arr[0];
}
const str = firstElement([‘str’]);
// const str: string
const bool = firstElement([true]);
// const bool: boolean
const num = firstElement([1]);
// const num: number
通过给函数添加一个类型参数 T
,并且在两个地方使用它,我们就在函数的输入(即数组)和函数的输出(即返回值)之间创建了一个关联。现在当我们调用它,一个更具体的类型就会被判断出来;
3.1 推断(Inference)
注意在上面的例子中,我们没有明确指定T
的类型,类型是被 TypeScript 自动推断出来的。
我们也可以使用多个类型参数,举个例子:
const res = map([1, 2, 3, 4], (val) => val.split(“,”));
// (parameter) val: number
// 类型“number”上不存在属性“split”。
注意在这个例子中,TypeScript 既可以推断出 Input 的类型是number ,此时自然不能使用split方法。
3.2 约束(Constraints)
有的时候,我们想关联两个值,但只能操作值的一些固定字段,这种情况,我们可以使用**约束(constraint)**对类型参数进行限制。
让我们写一个函数,函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个number类型的length属性。我们使用extends
语法来约束函数参数:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
const res1 = longest([1, 2, 3], 1);
// ❌ 类型“number”的参数不能赋给类型“{ length: number; }”的参数。
const res2 = longest([1, “2”, 3], [1]);
// const res2: (string | number)[]
const res3 = longest(‘yuguang’, ‘余光’);
// const res3: “yuguang” | “余光”
正是因为我们对Type做了{ length: number }
限制,我们才可以被允许获取 a b参数的 .length 属性。没有这个类型约束,我们甚至不能获取这些属性,因为这些值也许是其他类型,并没有 length 属性。
基于传入的参数,longerArray和 longerString 中的类型都被推断出来了。
3.3 泛型约束实战(Working with Constrained Values)
这是一个使用泛型约束常出现的错误:
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
// 不能将类型“{ length: number; }”分配给类型“Type”。
// “{ length: number; }” 可赋给 “Type” 类型的约束
// 但可以使用约束 “{ length: number; }” 的其他子类型实例化 “Type”。
}
}
这里很绕,我也理解了很久,欢迎大家讨论交流哦~
其中的问题就在于函数理应返回与传入参数相同类型的对象,而不仅仅是符合约束的对象。我们可以写出这样一段反例:
3.4 声明类型参数 (Specifying Type Arguments)
TypeScript 通常能自动推断泛型调用中传入的类型参数,但也并不能总是推断出。举个例子,有这样一个合并两个数组的函数:
function combine(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
// 如果你像下面这样调用函数就会出现错误:
const arr1 = combine([1, 2, 3], [“hello”]);
// 不能将类型“string”分配给类型“number”
// 而如果你执意要这样做,你可以手动指定 Type:
const arr2 = combine<string | number>([1, 2, 3], [“hello”]);
尽管写泛型函数很有意思,但也容易翻车。如果你使用了太多的类型参数,或者使用了一些并不需要的约束,都可能会导致不正确的类型推断。
4.1 类型参数下移(Push Type Parameters Down)
下面这两个函数的写法很相似:
function firstElement1(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]);
分析:
- 第一个函数可以推断出返回的类型是
number
,但第二个函数推断的返回类型却是 any,因为 TypeScript 不得不用约束的类型来推断 arr[0] 表达式,而不是等到函数调用的时候再去推断这个元素。
4.2 使用更少的类型参数 (Use Fewer Type Parameters)
这是另一对看起来很相似的函数:
// good
function filter1(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
// bad
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
我们创建了一个并没有关联两个值的类型参数 Func,这是一个危险信号,因为它意味着调用者不得不毫无理由的手动指定一个额外的类型参数。Func 什么也没做,却导致函数更难阅读和推断。
4.3 类型参数应该出现两次 (Type Parameters Should Appear Twice)
有的时候我们会忘记一个函数其实并不需要泛型
// bad
function greet(s: Str) {
console.log("Hello, " + s);
}
// good
function greet(s: string) {
console.log("Hello, " + s);
}
记住:类型参数是用来关联多个值之间的类型。如果一个类型参数只在函数签名里出现了一次,那它就没有跟任何东西产生关联。如果一个类型参数仅仅出现在一个地方,强烈建议你重新考虑是否真的需要它。
-
可选参数
-
参数默认值
-
剩余参数
-
参数解构
5.1 可选参数
如果你已经对函数的参数类型声明,那么输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
与接口中的可选属性类似,我们用?
表示可选的参数:
function hello(name1: string, name2: string): void {
console.log(hello! ${name1} ${name2 ? "and" + " " + name2 : ""}
);
}
hello(“余光”);
// 应有 2 个参数,但获得 1 个
// 未提供 “name2” 的自变量。
function hello1(name1: string, name2?: string): void {
console.log(hello! ${name1} ${name2 ? "and" + " " + name2 : ""}
);
}
hello1(“余光”);
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
5.2 参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
function hello(name1: string = “余光”, name2: string = “yuguang”): void {
console.log(hello! ${name1} ${name2 ? "and" + " " + name2 : ""}
);
}
hello(); // hello! 余光 and yuguang
hello(“小明”); // hello! 小明 and yuguang
是不是又拉回到了我们熟悉的领域?
5.3 剩余参数
ES6 中,可以使用 …rest 的方式获取函数中的剩余参数(rest 参数):
function func(a, …arg) {
console.log(a);
console.log(arg);
}
func(1, 2, 3, 4); // 1, [2, 3, 4]
**注意:**rest参数只能是最后一个参数
5.4 参数解构
你可以使用参数解构方便的将作为参数提供的对象解构为函数体内一个或者多个局部变量,在 JavaScript 中,是这样的:
type Man = { name: string; age: number };
function sum({ name, age }: Man) {
console.log(name + age);
}
sum({ a: 10, b: 3, c: 9 });
// 类型“{ a: number; b: number; c: number; }”的参数不能赋给类型“Man”的参数。
// 对象文字可以只指定已知属性,并且“a”不在类型“Man”中。ts
在解构语法后,要写上对象的类型注解。
这里介绍一些也会经常出现的类型。像其他的类型一样,你也可以在任何地方使用它们,但它们经常与函数搭配使用。
6.1 void
void 表示一个函数并不会返回任何值,当函数并没有任何返回值,或者返回不了明确的值的时候,就应该用这种类型。
function noop() {
return;
}
在 JavaScript 中,一个函数并不会返回任何值,会隐式返回 undefined,但是 void 和 undefined 在 TypeScript 中并不一样。
6.2 object
这个特殊的类型 object 可以表示任何不是原始类型(primitive)的值 (string、number、bigint、boolean、symbol、null、undefined)。object 不同于空对象类型 { },也不同于全局类型 Object。很有可能你也用不到 Object。
object
不同于Object
,总是使用object
!
6.3 unknown
unknown
类型可以表示任何值。有点类似于 any,但是更安全,因为对 unknown 类型的值做任何事情都是不合法的:
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
// 类型“unknown”上不存在属性“b”
}
结尾
学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。