基础概念
JavaScript 的缺陷
任何新技术的出现都是为了解决原有技术的某个痛点。
ES6、7、8等的推出,每次都会让 JavaScript 这门语言更加现代、更加安全、更加方便。但是直到今天,JavaScript 在类型检测上依然是毫无进展。
认识 TypeScript
TypeScript 的特点
大前端的发展趋势
TypeScript 的编译运行环境
编译环境
我们需要在电脑上安装TypeScript,这样就可以通过TypeScript的Compiler将其编译成JavaScript;
所以,我们需要先可以先进行全局的安装:
# 安装命令
npm install typescript -g
# 查看版本
tsc --version
运行环境
如果我们每次为了查看TypeScript代码的运行效果,都通过经过两个步骤的话就太繁琐了:
- 第一步:通过tsc编译TypeScript到JavaScript代码;
- 第二步:在浏览器或者Node环境下运行JavaScript代码;
是否可以简化这样的步骤呢?
- 比如编写了TypeScript之后可以直接运行在浏览器上?
- 比如编写了TypeScript之后,直接通过node的命令来执行?
上面我提到的两种方式,可以通过两个解决方案来完成:
- 方式一:通过webpack,配置本地的TypeScript编译环境和开启一个本地服务,可以直接运行在浏览器上;
- 方式二:通过ts-node库,为TypeScript的运行提供执行环境;
webpack配置
方式一在之前的TypeScript文章中我已经有写过,如果需要可以自行查看对应的文章;
https://mp.weixin.qq.com/s/wnL1l-ERjTDykWM76l4Ajw;
使用 ts-node
# 安装ts-node
npm install ts-node -g
# 另外ts-node需要依赖 tslib 和 @types/node 两个包:
npm install tslib @types/node -g
# 现在,我们可以直接通过 ts-node 来运行TypeScript的代码:使用ts-node
ts-node math.ts
变量的声明
声明了类型后 TypeScript 就会进行类型检测,声明的类型可以称之为类型注解;
var/let/const 标识符: 数据类型 = 赋值;
注意数据类型的大小写,小写就是 ts 定义的数据类型,大写指的是 ECMA 定义的包装类。
let msg1: string = "hhh" // string是TypeScript中定义的字符串类型
let msg2: String = "hhh" // String是ECMAScript中定义的一个类
变量的类型推导(推断)
定义变量的时候,我们会初始化变量。给变量初始化时的数据是什么数据类型,则 ts 就会推断这个变量的类型就是初始化时的类型。这样就可以方便不需要将类型写出来。
let msg = 123 // 初始化是数字,则推断为 number 类型
msg = "hhh" // 报错
数据类型
JavaScript和TypeScript的数据类型
TypeScript是JavaScript的一个超集,数据类型自然也是一样。
JavaScript 中的类型
number 类型
boolean 类型
string 类型
Array 类型
数组类型除了定义自身是个数组类型外,还需要定义数组内容是什么类型。另外在 js 中尽管没有类型概念,但是最好存放在数组中的类型是同一类型。
定义数据类型有两种方式:
const names1: Array<string> = [] // 不推荐(react jsx中是有冲突 <div></div>)
const names2: string[] = [] // 推荐
names.push("abc")
names.push(123) // 报错
Object 类型
对象类型没有意义,因为对象本身就是作为一个复杂数据类型出现,所以对象中可以存任意类型值。
具体到 ts 中,对象中的键值对会自动推断类型。
const obj = {
name: 'zs',
age: 30
}
//类型
const obj = {
name: string;
age: number;
}
Symbol 类型
null 和 undefined 类型
js 中的 6 个假值:false、null、0、""、undefined、NaN
。空数组[]
和空对象{}
都为真值 true。
js 中有 null 和 undefined 两个类型,ts 也有,并且这两个类型的值也只有 null 和 undefined。
注意:null 的类型推断,因为我们可能用 null 初始化变量,但是实际想要保存的是其他类型的值。所以 ts 就把 null 值的类型推断为 any。
let msg = null
msg = 123 // 成功
// 显示确定类型
let hhh: null = null
hhh = 123 // 报错
如果是其他类型,也是可以用 null 进行赋值的,但是开启 ts 的严格模式后编译会报错。严格模式默认开启,也可以在 tsconfig.json 中配置关闭。
TypeScript 中额外的类型
any 类型
因为类型推断的存在,其实 ts 中的变量已经不能随意接受其他值了。如果我们想要让变量不受类型限制,就可以设置类型为 any,回到最初的模样。
// 在不想给某些JavaScript添加具体的数据类型时(和原生的JavaScript代码是一样)
let message: any = "Hello World"
message = 123
message = true
message = { }
unknown 类型
unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量。
any 也是描述类型不确定的变量,那这和 unknown 有啥区别?
any 和最初的 js 一样,没有任何限制。想干嘛干嘛。但是 unknown 它限制了 unknown 类型的变量只能赋值给 unknown 类型或者 any 类型。这就防止了它到处乱用。
function foo() {
return "abc"
}
function bar() {
return 123
}
// unknown类型只能赋值给any和unknown类型
// any类型可以赋值给任意类型
let flag = true
let result: unknown // 最好不要使用any
if (flag) {
result = foo()
} else {
result = bar()
}
let message: string = result // 报错
void 类型
void通常用来指定一个函数是没有返回值的,那么它的返回值就是 void 类型。
函数我们没有写任何返回类型,那么它默认返回值的类型就是 void ,我们也可以显示的来指定返回值是 void:
- js 没写返回值,默认返回 undefined ```javascript // 显示指出返回类型为 void function foo(num: number): void { console.log(num) }
// 默认返回 void function fn(num: number) { console.log(num) }
<a name="d0zjv"></a>
### never 类型
表示永远不会发生,当一些错误可能逃逸的时候,我们就能在错误逃逸的执行路径上增加一个违反 never 语句。当错误逃逸,按这条路径执行到 never 的语句,就会因为 never 报错,而终止执行。<br />相当于把 never 当一个报警器了。
```javascript
// 假如封装了一个核心函数
// 参数类型只支持 string 和 number
function handleMessage(message: string | number) {
switch (typeof message) {
case 'string':
console.log("string处理方式处理message")
break
case 'number':
console.log("number处理方式处理message")
break
default:
}
}
// 张三找到了源码,并且给函数的参数类型添加一个可选的 boolean,然后传入布尔值。
function handleMessage(message: string | number | boolean)
handleMessage(true)
// 这个时候,函数可以接收 bool 值,但其实函数内部并不会对 bool 值有任何处理,而且函数还不报错
// 为了规范使用者使用我们的库函数,可以增加容错措施
function handleMessage(message: string | number | boolean) {
switch (typeof message) {
case 'string':
console.log("string处理方式处理message")
break
case 'number':
console.log("number处理方式处理message")
break
default:
// 增添容错检测,正常传入 string 和 number 不会执行到这
// 一旦传入其他类型,就会执行到这,但是赋值操作是 never永不发生,所以会报错
const check: never = message
}
}
tuple 类型
元组类型允许表示一个已知元素数量和类型的数组,很多语言中也有这种数据类型,比如Python、Swift等。
之前定义数组类型:const x: string[];
表示数组中元素类型必须都是 string。而 tuple 类型可以让我们定制元素类型。
根据索引值获取到的元素可以得到对应的类型;
元组类型的数组定义时是有确切长度的,如果越界添加元素,则新元素类型必须是前面元素类型中的一种。
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
// 越界添加元素
x[3] = 'hhh' // ok,因为 x[3] 能接收的类型为 string|number
x[4] = false // error
那么tuple在什么地方使用的是最多的呢?
- tuple通常用来定义返回值的类型,在使用的时候会非常的方便;
比如一个函数它返回一个数组,数组里面有一个 any 类型的元素和一个函数。现在要 ts 要返回这个数组,那怎么定义这个数组呢?
- 定义成 any 类型的数组,可以解决问题,但是失去类型控制的意义了,回到解放前的 js
- 不返回数组了,返回一个对象,值和函数作为对象的属性。Java 就是这么干的,但是不够灵活,因为用的时候必须使用对象中定义好的 key,要不然访问不到对象中的属性值。
返回 tuple 类型的数组,只规定了类型,用户拿到了可以随便取名字用。 ```javascript // 定义一个函数,接收一个值,返回一个数组,数组中第一个元素为原来的值,第二个元素为设置新值的函数 function useState(state: any) { let currentState = state const changeState = (newState: any) => { currentState = newState }
// 定义数组 tuple,第一个元素为任意值,第二个为函数:该函数接收一个任意值参数,并且无返回值 const tuple: [any, (newState: any) => void] = [currentState, changeState] return tuple }
// 解构数组获取里面的元素 const [counter, setCounter] = useState(10); setCounter(1000)
// 使用时,可以随意取名接收元素,比对象的方式灵活简洁 const [title, setTitle] = useState(“abc”)
<a name="A4Gt6"></a>
# 函数中的类型
<a name="AUreC"></a>
## 参数类型
声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型:
```javascript
function sum(num1: number, num2: number) {
return num1 + num2
}
// 参数类型和个数必须正确
sum(1, 2)
函数的返回值类型
我们也可以添加返回值的类型注解,这个注解出现在函数列表的后面:
function sum(num1: number, num2: number): number {
return num1 + num2
}
和变量的类型注解一样,我们通常情况下不需要返回类型注解,因为 TypeScript 会根据 return 返回值推断函数的
返回类型。某些第三方库处于方便理解,会明确指定返回类型,但是这个看个人喜好。
匿名函数的参数
匿名函数的参数类型,因为执行环境可能不一样,所以我们不仅可以给它明确指定,还可以让它自己根据上下文推导参数类型。我们也可以称这种类型为上下文类型。
const names: string[] = ["abc", "cba", "nba"]
// item根据上下文的环境推导出来的, 这个时候可以不添加的类型注解
names.forEach(function(item) { // 很明显 names 中都是 string,所以 item 不用手动指定类型
console.log(item.split(""))
})
对象类型
如果我们希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?
我们可以使用对象类型;
// Point: x/y -> 对象类型
// {x: number, y: number}
function printPoint(point: {x: number, y: number}) {
console.log(point.x);
console.log(point.y)
}
printPoint({x: 123, y: 321})
在这里我们使用了一个对象来作为类型:
- 在对象我们可以添加属性,并且告知TypeScript该属性需要是什么类型;
- 属性之间可以使用 , 或者 ; 来分割,最后一个分隔符是可选的;
- 每个属性的类型部分也是可选的,如果不指定,那么就是any类型;
可选类型
对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?
: ```javascript function printPoint(point: {x: number, y: number, z?: number}) { // z 可选 console.log(point.x) console.log(point.y) console.log(point.z) }
printPoint({x: 123, y: 321}) printPoint({x: 123, y: 321, z: 111})
function foo(msg?: string) { // msg 可选 console.log(msg) } foo() foo(‘hhh’)
<a name="vGxQn"></a>
## 联合类型
联合类型(Union Type)是由两个或者多个其他类型组成的类型;表示变量可以是这些类型中的任何一个类型;联合类型中的每一个类型被称之为联合成员(union's members );
联合类型虽然不像 any 一样没有限制,但类型依然具有不确定性,还是可能带来问题。所以代码逻辑中依然需要判断实参具体是联合类型中的哪一种,这种代码结构我们称为`narrow 缩小`,因为它缩小了类型范围。
```javascript
// number|string|boolean 联合类型
function printID(id: number|string|boolean) {
// 使用联合类型的值时, 需要特别的小心
// narrow: 缩小
if (typeof id === 'string') {
// 确定了 id 一定是string类型
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
printID(123)
printID("abc")
printID(true)
其实可选类型本质就是联合类型联合了 undefined,联合类型需要显示传入 undefined,不能不传入任何参数,而可选类型可以什么都不传入,其实默认传入了 undefined。
function foo(msg?: string)
function foo(msg: string|undefined)
类型别名
联合类型或者对象类型,写起来太长了,不直观。所以可以给类型起个别名,类似 C语言中的 #define。
关键字为type
// type用于定义类型别名(type alias)
type IDType = string | number | boolean // 可选类型别名
type PointType = { // 对象类型别名
x: number
y: number
z?: number
}
function printId(id: IDType) {
}
function printPoint(point: PointType) {
}
类型断言 as
有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)。
TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型
- 语法:
as 类型
比如我们通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它
具体的类型:
// 假设 why 所在的元素为 img,但是 ts 不知道
const el = document.getElementById("why") as HTMLImageElement // 断言明确元素类型为 img
// 一般的元素没有 src 属性,所以 ts 会报错
el.src = "url地址"
断言也会多应用于多态。
class Person { }
// Student 继承 Person
class Student extends Person {
studying() {
console.log(123);
}
}
function sayHello(p: Person) {
(p as Student).studying() // 接收父类,然后断言向下转型具体的子类才能调用子类的方法
}
sayHello(new Student()) // 123
// 3.了解: as any/unknown
const message = "Hello World"
const num: number = (message as unknown) as number
非空类型断言 !
我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言:
非空断言使用的是!
,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测
function printMessageLength(message?: string) {
// message 是可选的,如果没传入,则是 undefined.length 报错,所以 ts 编译不通过
// console.log(message.length)
// if (message) { // 可以 if 判断增添容错
// console.log(message.length)
// }
console.log(message!.length) // 非空断言,断定 message 为非空,ts 校验通过
}
我对 ts 编译器断言,我断定这个形参有值,你只管按我的决定去办。
但是我可能断言错了,逃过了编译,实际执行时还是报错,所以严谨的写法是使用可选链。
可选链的使用
可选链并不是 TypeScript 独有的特性,它是ES11(ES2020)中增加的特性:
可选链使用可选链操作符 ?.
它的作用是当对象的属性不存在时,会短路,表达式直接返回undefined,如果存在,那么才会继续执行;
// info 是一个隐藏的对象
const info = {
name: "why",
// friend: {
// girlFriend: {
// name: "hmm"
// }
// }
}
// 我们预期 info 对象有 friend 属性,但是某种原因没了
// 那就会立即因 undefined.girlFriend 导致报错,终止后面代码的执行
console.log(info.friend.girlFriend.name)
// 之前会用 &&与运算 和 if 先判断属性是否存在,但是这样太烦了
if (info && info.friend && info.friend.girlFriend) {
console.log(info.friend.girlFriend.name)
}
// ES11 提供了可选链(Optional Chainling)
// 以 ?. 的方式调用,若中间某个属性不存在,则会立即停止调用,并返回 undefined,不影响后续代码
console.log(info.friend?.girlFriend?.name) // undefined
console.log('其他的代码逻辑')
字面量类型
字面量类型就是相当于自己定义了一个类型,且属于这个类型的值只有定义类型时的值。let msg: hhh = 'hhh'
hhh 就是字面量类型,并且属于 hhh 类型的值只有 hhh。
其实 const 定义的变量,ts 类型推导后的都是字面量类型:const msg = 'hhh'
msg 的类型推导出来的不是 string,而是字面量类型 hhh。
这种字面量类型就像 const 一样定义不可修改的变量,有啥意义呢?
它的意义就是和联合类型一起达到类似枚举的效果。
type method = 'GET'|'POST'
function request(requestMethod: method) // requestMethod 的取值就只能是 get 或者 post 了
字面量推理
来看下面的代码:
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
const options = {
url: "ahcheng.top",
method: "POST"
}
// options.method 会传参失败,因为 request 要求的是字面量类型 Method
// 而 options.method 类型推导为 string
request(options.url, options.method)
// 三种解决办法
// 1. 对 options.method 进行断言
request(options.url, options.method as Method)
// 2. 定义 options 对象时,就按类型要求定义
type Request = {
url: string,
method: Method
}
const options: Request {
url: "ahcheng.top",
method: "POST" // method 类型不在为 string,而是 Method
}
// 3. 字面量类型推导添加 as const
const options = {
url: "ahcheng.top",
method: "POST"
} as const // 添加后,method 类型就推导为字面量类型 POST
类型缩小
什么是类型缩小呢?
- 类型缩小的英文是 Type Narrowing;
- 我们可以通过类似于 typeof padding === “number” 的判断语句,来改变TypeScript的执行路径;
- 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小;
- 而我们编写的 typeof padding === “number 可以称之为 类型保护(type guards);
常见的类型保护有如下几种:
- typeof
- 在 TypeScript 中,检查返回的值typeof是一种类型保护:因为 TypeScript 对如何typeof操作不同的值进行编码。
- 平等缩小(比如===、!==)
- 我们可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != ):
- instanceof
- JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”:
- in
- Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符
- 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true; ```javascript // 1.typeof的类型缩小 type IDType = number | string function printID(id: IDType) { if (typeof id === ‘string’) { console.log(id.toUpperCase()) } else { console.log(id) } }
// 2. 平等的类型缩小(=== == !== !=/switch) type Direction = “left” | “right” | “top” | “bottom” function printDirection(direction: Direction) { // 1.if判断 // if (direction === ‘left’) { // console.log(direction) // } else if ()
// 2.switch判断 // switch (direction) { // case ‘left’: // console.log(direction) // break; // case … // } }
// 3.instanceof function printTime(time: string | Date) { if (time instanceof Date) { console.log(time.toUTCString()) } else { console.log(time) } }
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)
// 4. in 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)
<a name="Dbu0g"></a>
# 函数类型的补充
在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进<br />行传递)。<br />在定义函数的过程中,也可以给函数添加类型。我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型;
```javascript
// 1.函数作为参数时, 在参数中编写类型
function foo() {}
type FooFnType = () => void // 函数类型:没有参数,没有返回值
function bar(fn: FooFnType) {
fn()
}
bar(foo)
// 2.定义常量时, 编写函数的类型
type AddFnType = (num1: number, num2: number) => number
const add: AddFnType = (a1: number, a2: number) => {
return a1 + a2
}
在某些语言中,可能参数名称num1和num2是可以省略,但是TypeScript是不可以的:
参数的可选类型
我们可以指定某个参数是可选的,并且可选参数必须写在必传参数的后面。
// 可选类型是必须写在必选类型的后面的
// 可选类型其实就是和 undefined 的联合类型: y -> undefined | number
function foo(x: number, y?: number) {
}
foo(20, 30)
foo(20)
默认参数
从ES6开始,JavaScript是支持默认参数的,TypeScript也是支持默认参数的:
// 参数顺序:必传参数 - 有默认值的参数 - 可选参数
function foo(y: number, x: number = 20) {
console.log(x, y)
}
foo(30)
剩余参数
从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是可以被推导出来 info对象(TypeScript推导出来)
const info = {
name: "why",
eating() {
console.log(this.name + " eating")
}
}
info.eating()
但是对于某些情况来说,我们并不知道this到底是什么?
function eating(message: string) {
console.log(this.name + " eating", message);
}
const info = {
name: "why",
eating: eating,
};
info.eating("哈哈哈"); // 报错
这段代码运行会报错的:
- 这里我们再次强调一下,TypeScript 进行类型检测的目的是让我们的代码更加的安全;
- 所以这里对于 sayHello 的调用来说,我们虽然将其放到了info中,通过info去调用,this其实也是指向info对象的;
- 但是对于TypeScript编译器来说,这个代码是非常不安全的,因为我们也有可能直接调用函数,或者通过别的对象来调用函数;
这个时候,通常TypeScript会要求我们明确的指定this的类型:
type ThisType = { name: string };
function eating(this: ThisType, message: string) {
console.log(this.name + " eating", message);
}
const info = {
name: "why",
eating: eating,
};
// 隐式绑定
info.eating("哈哈哈"); // why eating 哈哈哈
// 显示绑定
eating.call({name: "kobe"}, "呵呵呵") // kobe eating 呵呵呵
eating.apply({name: "james"}, ["嘿嘿嘿"]) // james eating 嘿嘿嘿
函数的重载
在TypeScript中,如果我们编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
我们可能会使用联合类型来编写,但是联合类型不能直接使用 + ,需要类型缩小。
function add(num1: string|number, num2: string|number) {
// return num1 + num2 // 联合类型不能使用 + 运算符
// 需要类型缩小
if (typeof num1 === "string" && typeof num2 === "string") {
return num1.length + num2.length
} else if (typeof num1 === 'number' && typeof num2 === 'number') {
return num1 + num2
}
}
通过联合类型有两个缺点:
- 进行很多的逻辑判断(类型缩小)
- 返回值的类型依然是不能确定,比如上面可能是字符串也可能是数字
为了实现这个需求,在TypeScript中,我们可以去编写不同的重载签名( overload signatures )来表示函数可以以不同的方式进行调用;一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;
在我们调用add 函数的时候,它会根据我们传入的参数类型来决定执行哪一个重载签名,然后和实现函数的函数体组合执行。
// 函数的重载: 函数的名称相同, 但是参数不同的几个函数, 就是函数的重载
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) // 走 number 类型的函数签名
const result2 = add("abc", "cba") // 走 string 类型的函数签名
console.log(result) // 50
console.log(result2) // 6
// 在函数的重载中, 实现函数是不能直接被调用的
// add({name: "why"}, {age: 18})
当然一般情况下,函数重载太麻烦了,所以不需要类型缩小的时候,一般使用联合类型。