本文整理来自深入Vue3+TypeScript技术栈-coderwhy大神新课,只作为个人笔记记录使用,请大家多支持王红元老师。
JavaScript和TypeScript的数据类型
我们经常说TypeScript是JavaScript的一个超集:
JavaScript类型 – string类型
string类型是字符串类型,可以使用单引号或者双引号表示:
同时也支持ES6的模板字符串来拼接变量和字符串:
默认情况下,如果可以推导出对应的标识符的类型时,一般情况下是不加类型注解的。
JavaScript类型 – number类型
数字类型是我们开发中经常使用的类型,TypeScript和JavaScript一样,不区分整数类型(int)和浮点型(double),统一为number类型。
let num: number = 123
num = 222
如果你学习过ES6应该知道,ES6新增了二进制和八进制的表示方法,而TypeScript也是支持二进制、八进制、十六进制的表示:
//十进制
let num1: number = 100 //100
//二进制
let num2: number = 0b100 //4
//八进制
let num3: number = 0o100 //64
//十六进制
let num4: number = 0x100 //256
console.log(num1, num2, num3, num4)
JavaScript类型 – boolean类型
boolean类型只有两个取值:true和false,非常简单。
JavaScript类型 – Array类型
数组类型的定义也非常简单,有两种方式:
// 一个数组中在TypeScript开发中, 最好存放的数据类型是固定的(string), 在数组中存放不同的类型是不好的习惯
// 类型注解: type annotation
// 泛型写法 不推荐(react jsx中是有冲突 )
const names1: Array = []
const names2: string[] = [] // 推荐
// 在数组中存放不同的类型是不好的习惯
// names.push("abc")
// names.push(123)
如果添加其他类型到数组中,那么会报错:
JavaScript类型 – Object类型
object对象类型可以用于描述一个对象:
但是从myinfo中我们不能获取数据,也不能设置数据:
所以一般情况下,我们使用对象就直接让它进行类型推导,如下:
const info = {
name: "why",
age: 18
}
console.log(info.name)
JavaScript类型 – Symbol类型
在ES5中,我们是不可以在对象中添加相同的属性名称的,比如下面的做法:
通常我们的做法是定义两个不同的属性名字:比如identity1和identity2。
ES6新增symbol类型,我们也可以通过symbol来定义相同的名称,因为Symbol函数返回的是不同的值:
const title1: symbol = Symbol("title")
const title2: symbol = Symbol('title')
const info = {
[title1]: "程序员",
[title2]: "老师"
}
JavaScript类型 – null和undefined类型
在 JavaScript 中,undefined 和 null 是两个基本数据类型,在TypeScript中,它们各自的类型也是undefined和null,也就意味着它们既是实际的值,也是自己的类型。也就是说null类型只有一个值null,undefined类型也只有一个值undefined。
TypeScript类型 - any类型
在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化,这个时候我们可以使用any类型(类似于Dart语言中的dynamic类型)。
any类型有点像一种讨巧的TypeScript手段,我们可以对any类型的变量进行任何的操作:包括获取不存在的属性、方法,可以给一个any类型的变量赋任何的值,比如数字、字符串的值。
如果对于某些情况的处理过于繁琐不希望添加规定的类型注解,或者在引入一些第三方库时,缺失了类型注解,这个时候我们可以使用any,包括在Vue源码中,也会使用到any来进行某些类型的适配。添加any类型后,其实就和原生的JavaScript代码是一样。
TypeScript类型 - unknown类型
unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量。
什么意思呢?我们来看下面的场景:
function foo() {
return "abc"
}
function bar() {
return 123
}
// any类型可以赋值给任意类型
// unknown类型只能赋值给any和unknown类型
let flag = true
let result: unknown // 最好不要使用any,因为any太灵活了
if (flag) {
result = foo()
} else {
result = bar()
}
//报错
let message: string = result
//报错
let num: number = result
console.log(result)
export {}
any类型和unknown类型的区别:
- any类型可以赋值给任意类型
- unknown类型只能赋值给any和unknown类型
解释:定义成any类型那么肯定可以赋值给任意类型,定义成unknown类型就只能赋值给any和unknown类型。unknown类型是TypeScript3.x出现的,目的就是为了防止我们随意赋值,比如把一个unknown类型赋值给string,这肯定是不行的。
TypeScript类型 - void类型
void通常用来指定一个函数是没有返回值的,那么它的返回值就是void类型。我们可以将null和undefined赋值给void类型,也就是函数可以返回null或者undefined。
这个函数我们没有写任何类型,那么它默认返回值的类型就是void的,我们也可以显示的来指定返回值是void。
TypeScript类型 - never类型
never 表示永远不会发生值的类型,比如一个函数是一个死循环或者抛出一个异常,那么这个函数会返回东西吗?不会,那么写void类型或者其他类型作为返回值类型都不合适,我们就可以使用never类型。
never有什么样的应用场景呢?这里我们举一个例子,但是它用到了联合类型,后面我们会讲到。
TypeScript类型 - Tuple类型
tuple是元组类型,很多语言中也有这种数据类型,比如Python、Swift等。
那么tuple和数组有什么区别呢?
- 首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中(可以放在对象或者元组中)。
- 其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型。
Tuple的应用场景
tuple通常可以作为返回的值,在使用的时候会非常的方便。
//使用泛型优化
function useState(state: T) {
let currentState = state
const changeState = (newState: T) => {
currentState = newState
}
//const info: [string, number] = ["abc", 18] 和下面类似
//返回元组类型
const tuple: [T, (newState: T) => void] = [currentState, changeState]
return tuple
}
//对返回值进行解构 counter是number类型, setCounter是函数类型
const [counter, setCounter] = useState(10);
setCounter(1000)
const [title, setTitle] = useState("abc")
// 定义函数类型
type MyFunction = () => void
// 使用函数类型
const foo: MyFunction = () => {}
函数的参数类型
函数是JavaScript非常重要的组成部分,TypeScript允许我们指定函数的参数和返回值的类型。
声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型:
函数的返回值类型
我们也可以添加返回值的类型注解,这个注解出现在函数列表的后面:
和变量的类型注解一样,我们通常情况下不需要返回类型注解,因为TypeScript会根据 return 返回值推断函数的返回类型。某些第三方库出于方便理解,会明确指定返回类型,这个看个人喜好。
匿名函数的参数类型
匿名函数与函数声明会有一些不同,当一个函数出现在TypeScript可以确定该函数会被如何调用的地方时,该函数的参数会自动指定类型。
// 通常情况下, 在定义一个函数时, 都会给参数加上类型注解的
function foo(message: string) {
}
const names = ["abc", "cba", "nba"]
// item根据上下文的环境推导出来的, 这个时候可以不添加的类型注解
// 上下文中的函数: 可以不添加类型注解
names.forEach(function(item) {
console.log(item.split(""))
})
我们并没有指定item的类型,但是item是一个string类型,这是因为TypeScript会根据forEach函数的类型以及数组的类型推断出item的类型,这个过程称之为上下文类型(contextual typing),因为函数执行的上下文可以帮助确定参数和返回值的类型。
对象类型
如果我们希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?我们可以使用对象类型。
在这里我们使用了一个对象来作为类型,在对象我们可以添加属性,并且告知TypeScript该属性需要是什么类型,属性之间可以使用 ,
或者 ;
来分割,最后一个分隔符是可选的,每个属性的类型部分也是可选的,如果不指定,那么就是any类型。
可选类型
对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?
。
事实上,可选类型可以看做是 类型 | undefined
的联合类型。
联合类型
TypeScript的类型系统允许我们使用多种运算符从现有类型中构建新类型,也就是联合类型(Union Type),联合类型是由两个或者多个其他类型组成的类型,表示可以是这些类型中的任何一个值,联合类型中的每一个类型被称之为联合成员(union's members)。
使用联合类型
传入给一个联合类型的值是非常简单的,只要保证是联合类型中的某一个类型的值即可。但是我们拿到这个值之后,我们应该如何使用它呢?因为它可能是任何一种类型。比如我们拿到的值可能是string或者number,我们就不能对其调用string上的一些方法,那么我们怎么处理这样的问题呢?
我们需要使用缩小(narrow)联合(后续我们还会专门讲解缩小相关的功能),TypeScript可以根据我们缩小的代码结构,推断出更加具体的类型。
类型别名
在前面,我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次,怎么解决这个问题呢?
比如我们可以使用type给对象类型起一个别名:
类型断言 as
有时候TypeScript无法获取具体的类型信息,这个时候我们需要使用类型断言(Type Assertions)。比如我们通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型。
另外案例:Person是Student的父类,关于多态。
class Person {
}
//学生可以学习
class Student extends Person {
studying() {
}
}
function sayHello(p: Person) {
// 多态
(p as Student).studying()
}
const stu = new Student()
//传入Student, Student肯定是Person
sayHello(stu)
TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型版本,此规则可防止不可能的强制转换:
按照提示先转为unknown就不会报错了,但是不推荐用。
非空类型断言 !
当我们编写下面的代码时,在执行ts的编译阶段会报错:
这是因为传入的message有可能是为undefined的,这个时候是不能执行方法的。
但是,如果我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言。非空断言使用的是 !
,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测,这类似swift的强制解包。
可选链的使用
可选链事实上并不是TypeScript独有的特性,它是ES11(ES2020)中增加的特性。
可选链使用可选链操作符 ?.
它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行。虽然可选链操作是ECMAScript提出的特性,但是和TypeScript一起使用更般配。
?? 和 !! 的作用
-
??
:空值合并操作符,它是ES11增加的新特性,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。和三目运算符有点像,但是比三目运算符更简洁。 -
!!
:将一个其他类型转换成boolean类型,类似于Boolean(变量)的方式,其实就是取反两次。
//空值合并操作符其实和逻辑或有点像,但是最开始逻辑或发明出来是用在if判断里面的
//所以如果在if判断中,使用逻辑或,其他地方使用空值合并操作符
if (message || "你好啊,李银河") {}
字面量类型
除了前面我们所讲过的类型之外,也可以使用字面量类型(literal types):
//这时候message是string类型
let message = "Hello World"
//这时候message是"Hello World"类型, "Hello World"也是可以作为类型的, 叫做字面量类型
const message = "Hello World"
// 字面量类型要求类型和值是一样的
const message: "Hello World" = "Hello World"
那么这样做有什么意义呢?
默认情况下这么做是没有太大的意义的,但是我们可以将多个类型联合在一起,这样传的参数就固定了,这就很有意义了。
// 字面量类型的意义, 就是必须结合联合类型
type Alignment = 'left' | 'right' | 'center'
let align: Alignment = 'left'
align = 'right'
function changeAlign(align: Alignment) {
console.log('修改方向:',align)
}
changeAlign(align)
字面量推理
我们来看下面的代码:
上面代码最后一行会报错,这是因为我们的info对象在进行字面量推理的时候,info.method其实是string类型的,但是request函数的method参数是字面量类型的,我们没办法将一个string类型赋值给一个字面量类型,所以会报错。
解决方式一:
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
const options = {
url: "https://www.coderwhy.org/abc",
method: "POST"
}
//直接将method转成更具体的类型
request(options.url, options.method as Method)
解决方式二:
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
const options = {
url: "https://www.coderwhy.org/abc",
method: "POST"
} as const
//通过as const将比较宽泛的类型转成字面量类型
//这样直接使用就行了
request(options.url, options.method)
解决方式三:
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
//定义参数对象类型
type Request = {
url: string,
method: Method
}
const options: Request = {
url: "https://www.coderwhy.org/abc",
method: "POST"
}
request(options.url, options.method)
一般我们使用方式三,虽然麻烦,但是更规范。