前言
我始终相信,任何新技术的出现都是为了解决原有技术的某些痛点。 毫无疑问,JavaScript是一门优秀且强大的语言。但是由于其历史背景的原因,Brendan Eich的设计存在着不少的缺点,但不可否认JavaScript也在一点点变好,比如ES5以后推出的 let、const、class、Promise、async/await
等等。
而直至今日JavaScript依旧没有加入的类型检测机制,实际上就是让我们在开发代码的时候让 错误出现提前。其实开发中需要有一个共识,那就是:
能在写代码期间发现错误就不要再代码编译期间发现
能在代码编译期间发现错误就不要在代码运行期间发现
能在开发期间发现错误就不要在测试期间发现
能在测试期间发现错误就不要在上线期间发现
TypeScript的优势就是可以做到 类型校验,能够在代码编写期间就直接报错,这是JavaScript做不到的。
在2014年Facebook也推出过flow来对JavaScript进行类型检测,Vue2.x就是采用的这种方式来进行类型检查。而现在TypeScript已经完全碾压flow,绝大部分需要类型检测的项目都会优先去选择TypeScript,学习它不仅仅可以为我们的代码增加类型约束,而且可以培养我们前端程序员也具备类型思维。
TypeScript编译环境
我们要知道,浏览器是无法直接编译运行TypeScript的。只有通过TypeScript环境将TS代码转换成JS代码才能正常运行,所以我们需要先安装一下TypeScript环境。
npm install typescript -g
安装完成后,我们可以通过tsc --version
查看它的版本号,然后去编写下方代码吧
// HelloTypeScript.ts
let message: string = "Hello World"
console.log(message)
export {}
写完之后又有了另外一个问题,我该怎么运行?直接引入TS文件吗,浏览器无法解析呀。
这时候就需要用到上面安装的tsc了,我们在终端输入tsc HelloTypeScript.ts
并回车,会发现文件夹内编译除了HelloTypeScript.js
文件,我们在html中引入该文件就可以执行了。
当然我们发现这种方式运行ts代码会比较繁琐,那么我们有两种解决方案 webpack/vite环境 与 ts-node。先介绍一下 ts-node,首先我们也需要进行安装:
npm install ts-node -g // 安装ts-node
npm install tslib @types/node -g // 安装ts-node的依赖包
安装完成后我们就可以直接通过ts-node main.ts
运行TypeScript代码了
TypeScript基本语法
变量声明
经过上述示例我们也知道了在TypeScript中定义变量需要指定 标识符 的类型,
声明类型后TypeScript会进行类型检测,声明的类型可以被称为。
所以一个完整的声明格式是:var/let/const 标识符: 类型注解 = 赋值;
类型推导
定义变量是直接进行赋值就会进行 类型推导,也就是如果不手动设置就会默认给变量设置个类型
但是let、const有所不同:
- let定义的变量,推导出来的是通用类型 [ string, number ... ]
- const定义的变量推导出来的是字面量类型
数组注解方式
注意事项:在开发中数组通常存放相同类型数据,切记
数组的类型注解方式有两种
1. string[]: 数组类型,并且数组中存放的字符串类型
2. Array<string>: 数组类型,通过泛型实现上面的效果
let names: string[] = ['aaa', 'bbb', 'ccc']
let nums: Array<number> = [111, 222, 333]
在开发中我们通常采用第一种方式,只有特殊情况下才会采用泛型数组写法
对象的注解方式
对象的注解方式还可以通过type和interface定义,这里先说下最简单的直接定义的方式
const myInfo: {
name: string,
age: number,
height: number // 因为是必传参数,所以这个对象其实是报错的
} = {
name: 'www',
age: 18
}
如果不想这样编写,其实可以跟js一样的直接定义,因为会进行类型推导。再补充一个小知识点,如果我们给一个对象定义类型为object
,那么只能证明它是一个对象,但是不可以获取和设置任何数据,所以千万不要直接给一个对象设置object类型,如果嫌麻烦可以设置any类型
。
null、undefined与symbol
null、undefined这两个类型和它们的值 是一样的,意味着是他们的值也是它们的类型
let n: null = null
let u: undefined = undefined
symbol则是通过Symbol定义相同名称
const s1: symbol = Symbol("title")
const s2: symbol = Symbol("title")
const person = {
[s1]: "张三",
[s2]: "王五"
}
函数的注解方式
函数中的形参都是外面传的,所以并不能进行类型推导。一般情况下都是明确指定类型的
function sum(n1: number, n2: number) {
return n1 + n2
}
但是函数的返回值是可以通过类型推导出来的,也是可以明确指定
function str(s1: string, s2: string): string {
return s1 + s2
}
匿名函数调用时最好不要添加类型注解,因为它本身就会根据上下文添加类型注解,如:
const names = ['aaa', 'bbb', 'ccc']
// 这里通过类型推导能够知道这个数组里面都是字符串
// 其实已经推导出来了,不需要再手动定义
// - item => string
// - index => number
// - arr => string[]
names.forEach((item, index, arr) => {
console.log(item.toUpperCase())
})
如果函数的形参是一个对象的话我们也可以进行注解
function pointzzzz(point: { x: number, y: number }) {
console.log(point.x)
console.log(point.y)
}
如果觉得太长了话也可以抽一个type
type PointType = { x: number, y: number }
function pointzzzz(point: PointType) {
console.log(point.x)
console.log(point.y)
}
TypeScript类型扩展
any类型
它是ts中比较重要的类型,意思是不限制标识符的任意类型,并且可以在标识符上进行任意操作,也就是相当于回到了JavaScript定义标识符。
但是并不推荐滥用,否则就会变成anyscript
。只有在某些特定情况下我推荐使用any类型,比如如下几种:
- 从服务器拿到的数据 极其繁琐,难以整理,词条巨多并无法确定类型
- 引入第三方库我们缺少对应的类型注解,我们也可以使用any
unknown类型
与any类型看上去是很相似的,都是无法确定类型,但是不同的是 any类型变量做任何操作都是可以的,但是unknown不可以!也就是不可以直接取属性、调方法等等
但是如果确实需要进行操作,那么就需要进行类型缩小,也就是类型校验后并进行对应操作
let foo: any = "aaa"
let bar: unknown = 'bbb'
console.log(foo.length) // 3
console.log(bar.length) // 代码直接编辑器报错
if(typeof bar === 'string') { // 进行类型缩小确定类型
console.log(bar.lenth) // 3
}
void类型
- 在ts中一个函数如果没有任何返回值,那么返回值的类型就是void。这个类型主要就是用来指定函数类型的返回值是void
- 如果我们定义了返回值void,那么其实也可以手动返回一个undefined,但是如果定义的是undefined类型那就只能且必须返回undefined
- 基于上下文类型推导出来的函数返回值类型为void,不强制返回值类型
never类型
出现场景:
-
首先,我们在开发中很少使用never类型,类型推导的时候可能会推导出never类型,基本是以下这些情况
- 函数死循环,永远不会返回任何东西
- 返回空数组
-
其次,我们在开发框架工具的时候可能会用到never类型,用来限制协同合作时可能会出现的错误
封装框架用来提示别人扩展工具时有一些提示(对没处理的地方直接报错)
function foo(message: string | number) { switch(typeof message) { case "string": console.log(message.length) case 'number': console.log(message) default: const check: never = message } }
如果别人想给这个函数扩展个boolean类型,直接在形参中添加
| boolean
的话defaule的check变量就会报错,因为没有写case进行新类型的处理
- 最后,在封装一些类型工具的时候可能会用到never(比如类型体操)
tuple类型
元组类型,多个类型组合在一起TypeScript数据类型。
一般在函数的返回值类型里用的多一些,最经典的就是React中的 useState,返回的就是一个元组类型(参数+函数)
我们会发现元组类型和数组类型很像,其实不然,他们之间还是有比较大的差别的。原来的数组我们通常只会把同类型数据放在一起,否则获取值后编译器不能知道明确类型,所以代码提示会很差。如果碰到需要将多类型数据放在一起的需求时通常会使用对象来解决,而ts中则可以通过 元组类型 解决
// 普通数组存储
const info: any[] = ['www', 18, 1.99]
const v1 = info[1] // v1类型为 any
// 元组类型存储
const infos: [string, number, number] = ['www', 18, 1.88]
const v2 = infos[1] // v2类型为 number
enum类型
枚举类型,他会把可能出现的之全都放到一个类型中,并且默认一次赋值0,1,2,3....
enum Direction {
LEFT,
RIGHT,
UP,
DOWN
}
// 上面这种方式等于
enum Direction {
LEFT = 0,
RIGHT = 1,
UP = 2,
DOWN = 3
}
// 也可以设置第一个值后依次递增
enum Direction {
LEFT = 100,
RIGHT,
UP,
DOWN
}
// 也可以从中间开始设置某个值
enum Direction {
LEFT,
RIGHT,
UP = 'TOP',
DOWN = 'BOTTOM'
}
TypeScript语法细节
联合类型
它是由两个或者多个其他类型组成的类型,表示可以是这些类型中任意一个值。联合类型中的每一个类型被称为联合成员。
它的使用很简单,但是通常会与类型缩小一起使用,就像下面这样:
function sends(id: number | string) { // 联合类型
if(typeof id === 'number') { // 类型缩小
console.log(id)
} else {
console.log(id.length)
}
}
类型别名
type关键字来定义类型别名,可以把过于臃肿的类型抽取出来,这样就可以使代码逻辑更清晰
function printABC(info: { a: number, b: string, c: string[] }) {
console.log(info.a, info.b, info.c)
}
// 可以抽取成这样
type infoType = {
a: number,
b: string,
c: string[]
}
function printABC(info: infoType) {
console.log(info.a, info.b, info.c)
}
接口声明
通过 interface关键字声明接口类型,和type类型别名很像,大多数情况下两个都可以使用。在接口中几乎所有的特性都可以在type中使用
两个关键字的主要区别:
- type类型适用范围更广,接口类型只能够声明对象
- 声明对象时type不可以多次声明, interface可以,所以对象类型中接口更加灵活
- interface可以实现继承
- 接口不仅仅可以继承,它也可以通过关键字implements实现的。实现接口则是更加方便我们去创建实例对象
所以一般声明对象类型就使用interface,声明其他类型就使用type
interface IPersion { // 定义接口
name: string,
age: number
}
interface IKun extends Ipersion {
height: number
}
// 三个参数一个都不能少,因为是继承了
const heizi: IKun = {
name: "xx",
age: 44,
height: 1.78
}
// 实现接口
class nPerson implements IKun {
name: string
age: number
height: number
}
交叉类型
交叉类型是需要同时满足类型A和类型B两种类型,是通过 & 来定义的。通常是对象才会使用到交叉类型
interface IKun {
name: string,
age: number
}
interface ICoder {
coding: () => void
}
const p1: IKun & ICoder = { // 需要同时满足两个接口的属性
name: 'www',
age: 16,
coding: function() {
console.log("coder")
}
}
类型断言
通过关键字as进行类型断言,手动确定我拿到的这个元素它是什么类型
它可以断言成一个更加具体的类型,也可以断言成更加不具体的类型(any, unknown)
// 通过标签选择器能够准确拿到元素的类型,这里就是 HTMLImageElement | null
const imgs = document.querySelector("img")
// 但是通过类选择器之类的拿到的元素就很宽泛了,所以这里的类型是 Element | null
const imgss = document.querySelector(".img")
// 进行类型缩小,杜绝null的存在后取值
if(typeof imgs !== "null") {
console.log(imgs.src) // 能够顺利取值
}
if(typeof imgss !== 'null') {
console.log(imgss.src) // 报错,因为编译器不知道imgss是一个图片元素
}
上面的案例如果我们确定类选择器拿到的一定是一个图片元素,那么就可以进行类型断言!
const imgss = document.querySelector(".img") as HTMLImageElement
甚至断言后都不需要进行类型缩小了,可以直接获取对应数据
非空类型断言
告诉编辑器我这个参数一定有值,必须确定的情况下才能使用,使用一定要慎重。例如:
interface IPerson {
name: string,
age: number,
friend?: { // 可选属性,不一定存在
name: string
}
}
const p1: IPerson = { // 声明p1
name: 'www',
age: 18,
friend: { // 拥有friend对象
name: 'bbb'
}
}
p1.friend.name = 'kobe' // 默认情况下报错,因为是可选的所以ts不知道有没有
// 两种解决方式 类型缩小和非空类型断言
// 类型缩小
if(p1.friend) {
p1.friend.name = "kobe"
}
// 非空类型断言 一定有这个参数
p1.friend!.name
字面量类型
可以限制某个变量必须传哪个或者哪些字面量,通常是联合类型一起使用,因为一个字面量类型没意义
// 例 1
type Direction = 'left' | 'right' | 'up' | 'down' // 定义方向类型
const d1: Direction = 'left'
// 例 2
type MethodType = 'get' | 'post' // 定义请求方式类型
function request(url: string, method: MethodType) {
...
}
函数类型
函数作为JS的一等公民,它可以作为参数也可以作为返回值传递。那么在TS中函数是否也可以有自己的类型?
答案是肯定的,我们可以编写来表示函数类型
type CalcType = (num1: number, num2: number) => number // 函数类型别名
function calc(calcFn: CalcType ) {
const num1 = 20
const num2 = 30
return calcFn(num1, num2)
}
TypeScript对函数传入的参数个数不进行检测
函数调用签名
除了上面的函数类型表达式,如果我们站在对象的角度来看待这个函数,它不仅仅可以调用而且也有其他的属性,比如:
interface IBar {
name: string
age: number
// 这就是函数调用签名
(n1: number): number
}
const bar: IBar = (n1: number): number => {
return n1
}
bar.name = 'aaa'
bar.age = 19
bar(123)
构造签名
有时候我们定义一个函数时是希望把它当作一个构造函数来使用的,那么在TS里就要通过构造签名来明确这个函数是可以new的
class Person {}
interface ICTORPerson {
new (): Person // 构造签名,返回一个Person类型对象
}
function factory(fn: ICTORPerson) {
const f = new fn()
return f
}
factory(Person)
函数的可选参数和默认值
函数的可选值通过?
来设置,跟对象属性是一样的写法。
函数的默认值跟ES6的写法是一样的,使用 =
来设置
function foo(x: number, y?: number, z = 199) {
if (y !== undefined) {
console.log(y)
}
}
foo(10, 20) // 不报错
foo(19) // 同样不会报错
foo(111, 222, undefined) // 依旧不报错
我们发现,默认值参数是可以接受undefined的,这就是语言设计的小细节。因为ts是帮我们进行开发的而不是给我们造成烦恼的,所以这些小细节是很多的。
TypeScript面向对象
成员修饰符
在TypeScript中类的属性和方法支持这几种修饰符:public private protected readonly
- public 修饰的是在任何地方可见、公有的属性或方法,默认属性就是public
- private 修饰的是在同一类中可见、私有的属性或方法
- protected 修饰的是仅在类自身和子类中可见、受保护的属性或方法
- readonly 修饰的是只读的属性,不可以进行修改
class Love {
name: string // 公开的
public age: number // 公开的
private _height: number // 私有的,有个不成文的规定就是前面加个下划线
protected friend: { name: string } // 受保护的
readonly children: string // 只读的
}
但是这种写法是比较麻烦的,我们可以通过使用参数属性的方法使代码更加整洁
// 参数属性
class Person {
constructor(public name: string, private age: number, readonly height: number) {}
running() {
console.log(this.age, 'running')
}
}
setter和getter
通常是对于类中的私有属性,为其添加一个set和get方法,用来进行拦截操作
class Abc {
private _name: string
private _age: number
constructor(name: string, age: number) {
this._name = name
this._age = age
}
get name() {
return this._name
}
set age(newVal: number) { // 设置的时候进行拦截操作
if(newVal >= 0 && newVal < 150) {
this._age = newVal
}
}
}
抽象类abstract
我们知道,面向对象有三大概念,封装继承多态,而继承又是多态的前提。
我们可以通过多态来实现更加灵活的调用,并且由于父类本身可能并不需要具体实现某些方法,只需要定义,我们就可以将这个方法定义成抽象方法。
抽象类会有以下几个特点:
- 抽象类不能被实例化(不能new)
- 抽象类可以包含抽象方法,也可以包含具体实现的方法
- 有抽象方法的类必须是一个抽象类
- 抽象方法必须被子类实现,否则该类必须是一个抽象类
abstract class Shape {
abstract getArea()
}
// 矩形
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super()
}
getArea() {
return this.width * this.height
}
}
// 圆形
class Circle extends Shape {
constructor(public radius: number) {
super()
}
getArea() {
return this.radius ** 2 * Math.PI
}
}
// 三角形
class Triangle extends Shape {
getArea() {
return 100
}
}
// 通用函数
function calcArea(shape: Shape) {
return shape.getArea()
}
calcArea(new Rectangle(10, 20))
calcArea(new Circle(20))
calcArea(new Triangle())
索引签名
用来声明该类型可以通过索引来进行访问数据
interface ICollection {
// 索引签名,要求该类型必须可以通过索引访问
// index: 自定义标识符,见名知意所以取index
// number:表示你想通过什么类型索引访问数据
// string:访问的索引值是什么类型的
[index: number]: string
length: number
}
function iteratorCollection(coll: ICollection) {
console.log(coll[0])
console.log(coll[1])
}
const names: string[] = ['foo', 'bar', 'baz']
const tuple: [string, string] = ['lamo', '18']
iteratorCollection(names) // 不报错
iteratorCollection(tuple) // 不报错
iteratorCollection({
name: 'lll',
age: 19,
length: 10
}) // 报错
TypeScript泛型编程
什么是泛型
泛型通俗点讲实际上就是一种类型参数化的语法,它可以将类型像形参一样由调用者传入。
function bar(arg: number | string) {
return arg
}
const res1 = bar("aaa")
const res2 = bar(111)
// 上面这种接受不同类型参数时,虽然使用联合类型可以避免报错,但是返回值也缺失了类型,对返回值进行操作时也有可能报错
// 下面这种做法就是把类型传入函数中,就可以明确类型,方便进行操作
function baz<Type>(arg: Type): Type {
return arg
}
const res3 = baz<string>('aaa')
const res4 = baz<number>(11234)
泛型接口和泛型类
在定义接口或者类的时候我们可能希望也是动态的传递一些类型,这时候就可以使用到泛型接口泛型类
interface IKun<T = string> { // 设置默认类型
name: T,
age: number,
slogin: T
}
// 传入字符串类型
const iKun1: IKun<string> = {
name: 'kunkun',
age: 18,
slogin: "你干嘛"
}
// 传入number类型
const iKun2: IKun<number> = {
name: 111,
age: 55,
slogin: 534
}
// 不传类型使用默认值
const ikun3:IKun = {
name: 'kunns',
age: 88,
slogin: "你太美"
}
// 泛型类
class Point<Type = number> {
x: Type
y: Type
constructor(x: Type, y: Type) {
this.x = x
this.y = y
}
}
const p1 = new Point(10, 20) // 类型推导number,直接传进去
const p2 = new Point<number>(20, 30) // 手动确定类型
const p3 = new Porin("111","222") // 类型推导string
泛型约束
有时候我们希望传入的类型有某些共性,但是这些共性不在同一类型中:
- 比如string和array都有length,或者某些对象也有length属性
- 那么只要是拥有length属性都可以作为我们的参数类型,那么怎么操作呢
想要保持泛型传过来的原来的类型并且需要对传入值的约束,这就需要用到extends关键字了
interface ILength {
length: number
}
// 如果不继承自ILength那就什么都可以传了,不需要有length属性
// Type 主要用于记录已经成功传入参数的类型
// ILength 用于约束传入的参数,它有没有length属性
function getInfo<Type extends ILength>(args: Type): Type {
return args
}
const info1 = getInfo("aaa")
const info2 = getInfo(['aaa', 'bbb', 'cc'])
const info3 = getInfo({ length: 100 })
泛型参数约束
在JS中我们通过key获取对象属性值时,即使是一个不存在的key那么在编写代码期间也不会报错,但是在ts中可以通过泛型参数约束来实现的
function getObjectProperty<O, K extends keyof O>(obj: O, key: K) {
return obj[key]
}
const info = {
name: 'foo',
age: 18,
height: 1.88
}
const name = getObjectProperty(info, 'name') // 不报错
const address = getObjectProperty(info, 'address') // 报错
export {}
上述代码会报错类型“"address"”的参数不能赋给类型“"name" | "age" | "height"”的参数。
这也就说明,keyof会将对象所有的key作为一个字面量联合类型返回
映射类型
有的时候一个类型需要基于另一个类型,但是你又不想拷贝一份,这时候就可以考虑使用映射类型
- 大部分内置的工具都是通过映射类型来实现的
- 大多数类型体操题目也是通过映射类型来完成的
映射类型建立在索引签名的语法上: - 映射类型就是使用了属性keys联合类型的泛型
- 属性keys多是通过keyof创建,然后循环遍历键名创建的一个类型
interface IPerson {
name: string
age: number
}
type MapType<T> = {
[property in keyof T]: T[property]
}
type NewPerson = MapType<IPerson>
那么我们为什么要拷贝呢?
答案其实是因为映射类型可以对拷贝的类型进行修饰,可以在不改变原类型的情况下添加一些修饰符:
interface IPerson {
name?: string
age: number
}
type MapType<T> = {
+readonly [property in keyof T]-?: T[property]
}
type NewPerson = MapType<IPerson>
// type NewPerson = {
// readonly name: string;
// readonly age: number;
// }
我们能看到原类型IPerson的name属性是可选类型,但是映射类型中可以通过-?
来删除这个可选特性,并且给所有属性加上readonly
特性