本文整理来自深入Vue3+TypeScript技术栈-coderwhy大神新课,只作为个人笔记记录使用,请大家多支持王红元老师。
类型缩小
什么是类型缩小呢?
类型缩小的英文是 Type Narrowing,我们可以通过类似于 typeof padding === "number" 的判断语句,来改变TypeScript的执行路径,在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为缩小,而我们编写的 typeof padding === "number 可以称之为 类型保护(type guards)。
常见的类型保护有如下几种:
- typeof
- 平等缩小(比如===、!==)
- instanceof
- in
- 等等...
typeof
在 TypeScript 中,检查返回的值typeof是一种类型保护,因为我们可以根据不同的值进行不同的编码。
平等缩小
我们可以使用switch或者相等的一些运算符来表达相等性(比如===, !==, ==, != )。
instanceof
JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”。
class Student {
studying() {}
}
class Teacher {
teaching() {}
}
function work(p: Student | Teacher) {
if (p instanceof Student) {
p.studying()
} else {
p.teaching()
}
}
const stu = new Student()
work(stu)
in
Javascript 有一个in运算符,用于确定对象是否具有带名称的属性,如果指定的属性在指定的对象或其原型链中,则in运算符返回true。
//定义对象类型别名
type Fish = {
swimming: () => void
}
type Dog = {
running: () => void
}
function walk(animal: Fish | Dog) {
if ('swimming' in animal) {
animal.swimming()
} else {
animal.running()
}
}
const fish: Fish = {
swimming() {
console.log("swimming")
}
}
walk(fish)
TypeScript函数类型
在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。那么在使用函数的过程中,函数是否也可以有自己的类型呢?我们可以编写函数类型的表达式(Function Type Expressions)来表示函数类型。
// 定义函数
function foo() {}
// 定义函数类型
type FooFnType = () => void
// 函数作为参数
function bar(fn: FooFnType) {
fn()
}
// 调用函数
bar(foo)
// 定义函数类型
type AddFnType = (num1: number, num2: number) => number
// 明确写出类型注解
const add: AddFnType = (a1: number, a2: number) => {
return a1 + a2
}
// calc函数有三个参数,前两个参数是number,第三个参数是一个函数,再将前两个参数传入到第三个函数参数中,然后将函数的结果返回
function calc(n1: number, n2: number, fn: (num1: number, num2: number) => number) {
return fn(n1, n2)
}
//调用
const result1 = calc(20, 30, function(a1, a2) {
return a1 + a2
})
console.log(result1)
//调用
const result2 = calc(20, 30, function(a1, a2) {
return a1 * a2
})
console.log(result2)
在上面的语法中 (num1: number, num2: number) => number,代表的就是一个函数类型。这个函数接收两个参数:num1和num2,并且都是number类型,并且返回一个number。在某些语言中,可能参数名称num1和num2可以省略,但是TypeScript是不可以的:
参数的可选类型
我们可以指定某个参数是可选的:
这个时候参数x依然是有类型的,其实就是联合类型:number | undefined,如下:
另外可选类型要在必传参数的后面,否则会报错:
默认参数
从ES6开始,JavaScript是支持默认参数的,TypeScript也是支持默认参数的:
这时候y的类型其实就是 number | undefined 联合类型。
有默认值的参数一般写在后面,以防有歧义,一般参数的顺序是:必传参数 - 有默认值的参数 - 可选参数。
剩余参数
从ES6开始,JavaScript也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中。
function sum(initalNum: number, ...nums: number[]) {
let total = initalNum
for (const num of nums) {
total += num
}
return total
}
console.log(sum(20, 30))
console.log(sum(20, 30, 40))
console.log(sum(20, 30, 40, 50))
可推导的this类型
this是JavaScript中一个比较难以理解和把握的知识点,coderwhy在公众号也有一篇文章专门讲解this:https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA
因为this在不同的情况下会绑定不同的值,所以对于它的类型就更难把握了,那么,TypeScript是如何处理this呢?我们先来看一个例子:
上面的代码是可以正常运行的,也就是TypeScript在编译时,认为我们的this是可以正确去使用的,TypeScript认为函数 sayHello 有一个对应的this的外部对象 info,所以在使用时,就会把this当做该info对象。
不确定的this类型
但是对于某些情况来说,我们并不知道this到底是什么?
这段代码运行会报错。这里我们再次强调一下,TypeScript进行类型检测的目的是让我们的代码更加的安全,所以这里对于 sayHello 的调用来说,我们虽然将其放到了info中,通过info去调用,this依然是指向info对象的,但是对于TypeScript编译器来说,这个代码是非常不安全的,因为我们也有可能直接调用函数,或者通过别的对象来调用函数。
指定this的类型
这个时候,通常TypeScript会要求我们明确的指定this的类型:
type ThisType = { name: string };
// 指定this的类型
function eating(this: ThisType, message: string) {
console.log(this.name + " eating", message);
}
const info = {
name: "why",
eating: eating,
};
// 隐式绑定
info.eating("哈哈哈");
// 显示绑定
eating.call({name: "kobe"}, "呵呵呵")
eating.apply({name: "james"}, ["嘿嘿嘿"])
函数的重载
在TypeScript中,如果我们编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?我们可能会这样来编写,但是其实是错误的:
如果我们通过联合类型:
function add(a1: number | string, a2: number | string) {
if (typeof a1 === "number" && typeof a2 === "number") {
return a1 + a2
} else if (typeof a1 === "string" && typeof a2 === "string") {
return a1 + a2
}
// return a1 + a2;
}
add(10, 20)
通过联合类型有两个缺点:
- 进行很多的逻辑判断(类型缩小)
- 返回值的类型依然是不能确定
那么这个代码应该如何去编写呢?
在TypeScript中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用,一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现。
Swift中的函数重载:函数名相同,参数个数不同 或 参数类型不同 或 参数标签不同。
TypeScript中的函数重载:函数名相同,但是参数不同的几个函数,就是函数的重载。
sum函数的重载
比如我们对sum函数进行重构,在我们调用sum的时候,它会根据我们传入的参数类型来决定执行函数体时,到底执行哪一个函数的重载签名。
// 函数的重载: 函数的名称相同, 但是参数不同的几个函数, 就是函数的重载
function add(num1: number, num2: number): number; // 没函数体
function add(num1: string, num2: string): string; // 没函数体
// 有实现体的函数,不能直接被调用
function add(num1: any, num2: any): any {
if (typeof num1 === 'string' && typeof num2 === 'string') {
return num1.length + num2.length
}
return num1 + num2
}
const result = add(20, 30) // 匹配第一个函数,最后会把第三个函数的实现当做函数体来执行
const result2 = add("abc", "cba") // 匹配第二个函数,最后会把第三个函数的实现当做函数体来执行
console.log(result) // 50
console.log(result2) //6
但是注意,有函数体的函数,是不能直接被调用的,所以上面第三个函数不能直接被调用,否则报错:
// 在函数的重载中, 实现函数是不能直接被调用的
add({name: "why"}, {age: 18})
TypeScript的函数重载,不能有函数体,并且有函数体的函数不能直接被调用,只能先匹配没函数体的函数,最后再调用有函数体的函数的实现,TypeScript的函数重载和其他语言的函数重载是有点不一样。
函数重载练习
我们现在有一个需求:定义一个函数,可以传入字符串或者数组,获取它们的长度。这里有两种实现方案。
方案一:使用联合类型来实现;
方案二:实现函数重载来实现;
这里肯定是使用联合类型来实现更方便,我们优先选择使用联合类型。