typescript 现在是越来越流行和普遍了,那么为什么在javascript的基础上又会出现一个typescript呢。typescript有些什么特点呢。今天我们一起来看一看
我们都知道,javascript是一个弱类型、动态类型的语言。他会带来哪些问题
变量的数据类型不是固定的,我们可以给一个变量赋值多种不同类型的值。
这样可能代码编写时会更简单,但当项目变大时,可能会给项目带来一定的隐患,也不便于多人合作
function add (a, b) {
return a + b
}
add(100, 200) // 300
add('a', 100) // 'a100' 这改变了我们函数定义的初衷
声明变量时,一旦给变量赋值了,那么变量的数据类型就已经确定了,不可在赋值其他类型的值。如果要赋值其他类型的值,必须要先进行强制数据类型转换
动态类型的语言在类型检查时,会在代码运行时检查。这就导致了当我们代码存在错误时,需要代码运行后,才能知道。
var obj = {
name: '张三'
}
console.log(obj.age)
这段代码会报错,因为obj并没有age属性。但只有在代码运行时才会报错。这时,我们可能需要一些约束来让我们更早的发现问题
静态类型在类型检查时,是在代码编译阶段进行的。能够更早的发现问题
npm i typescript -g // 安装typescript
通过指令 tsc index.ts 可以将ts转换为js运行,他会在特定的地方生成转换出来的js文件
通过tsconfig.json可以进行一定规则配置。从而让ts文件可以按照一定的规则转换成js文件
创建配置文件
通过 tsc --init 可以在当前目录下生成tsconfig.json文件
tsc --init
tsc -p ./tsconfig.json
typescript通过在数据定义时,加上类型,来给变量添加一定的约束,变量的类型一旦设定,将不可在给变量设置其他类型的数据,否则编译时就会报错
let num: number = 10 // 给num定义了number类型,后续只能赋值number类型的数据
num = 'abc' // 编译时会报错
number类型
// number类型 大小写都行 可接收十进制,二进制,八进制和十六进制数
let a: number = 10
let b: number = 0b1010 // 二进制 0b开头
let c: number = 0x1E0F3 // 十六进制 0x开头
let d: number = 0o12 // 八进制 0o开头
let e: number = NaN
let f: number = Infinity
string类型
// string类型
let str1: string = "双引号"
let str2: string = '单引号'
let str3: string = `模板字符串`
boolean类型
let flag1: boolean = true
let flag2: boolean = false
数组类型
两种方式
// Array<数组成员的数据类型>
let arr1: Array<number> = [1,2,3,4,5] // 代表arr1只能赋值数组给它,并且数组的成员都必须是number类型
// 数组成员的数据类型[]
let arr2: number[] = [1,2,3,4,5] // 效果和上面一模一样
元组
元组类型表示一个已知元素数量和类型的数组,数组内各个成员的类型不必相同
// 表示该arr只能接收一个长度是2 且第一个成员类型必须是number, 第二个成员类型必须是string的数组
let arr: [number, string] = [10, 'abc']
arr[0] = '123' // 会报错 只能赋值number类型
arr[1] = null // 会报错 只能赋值string类型
void类型
void类型表示没有任何类型,void类型的数据只能给他赋值undefined ,因此用处不大。
不过,因为他只能赋值undefined, 而一个函数 当没有返回值时会默认返回undefine, 故void可以用来定义没有返回值的函数
let reg: void = undefined
reg = 123 // 会报错
let function aaa(): void {
}
undefined类型
undefined类型只能赋一个值 undefined
let un: undefined = undefined
un = 123 // 会报错
null类型
null类型只能赋一个值 null
let nu: null = null
nu = 123 // 会报错
never类型
never 类型一般用来表示一个函数,但只能表示一些永远无法到达终点的函数
// never类型的函数必须存在一个无法到达的终点
function aaa (): never {
throw new Error('抛出一个错误')
}
// never类型的函数必须存在一个无法到达的终点
function bbb (): never {
while(true) {
}
}
never 类型是所有类型的子类型,故never类型的数据可以被赋值给任何类型的变量。但never类型的变量不可接收never以外任何类型的数据
let num: Number = 100
num = 200
var aaa = function (): never {
throw new Error('抛出一个错误')
}
num = aaa() // aaa是never类型,但他可以赋值给number类型
aaa = 10 // 会报错,因为aaa是never类型,不可以给他赋值任何never以外类型的数据
object类型
object类型不单单指对象类型,而是指除基础数据类型之外的所有类型(除number, string, null, undefined, boolean, symbol之外)
// 以下都可行
let obj: object = {
}
let arr: object = []
let fun: object = function () {
}
对象类型
对象类型,表示只能接收对象。并且可以给对象定义属性个数的约束,以及每个属性值类型的约束
let obj: {
name: string, age: number} = {
name: '张三'} // 会报错 当少赋值一个属性时,会报错
let obj2: {
name: string, age: number} = {
name: '张三', age: 18} // 不会报错
enum枚举类型
枚举类型这个可能不太好理解
这东西其实就是用来定义一组更语义化(让人更容易懂)的数据的
我举个例子:比如我们项目中,有一个状态字段,他可能有3种值,未开始,已完成,已取消。通常来说,表示这3种状态的变量我们是会赋值一个number类型的,比如0表示未开始,1表示已完成,2表示已取消。但是我们给一个变量(假如是state)赋值 0 或 1 或 2 是不是语义不够清晰啊 ,有时候看着state = 0 一时可能还想不起来是表示哪个状态。
枚举类型就是解决这个问题的
语法:enum 定义的类型名称 {}
// 定义一套状态集合,并赋初始值
enum States {
noStart = 0, // 未开始
isComplete = 1, // 已完成
isCancel = 2 // 已取消
}
let state: States = States.noStart // 等价于赋值1, 但这样语言化更好
// 等价于
let state: number = 0 // 上下两句是一样的,用哪个都可以,但是上面的明显语义化更好,让人一眼就能看出,当前是表示未开始
特别是在表示颜色时,比如代表success的颜色#67C23A,从字符上,看不出当前代表是成功还是警告还是失败,此时可以
enum Colors {
success = '#67C23A',
danger = '#F56C6C',
warn = '#E6A23C'
}
let color: Colors = Colors.success
// 比下面这句明显更容易让人理解
let color: string = '#67C23A'
any类型
表示任意类型,可以赋值任何类型的数据
let aaa: any = 1
let bbb: any = '123'
let ccc: any = false
ts中的类和es6 有一点不同的是,ts中的类中的属性,必须在类中定义,并指定类型
正确的做法
class Person {
name: string // 如果在constructor中给他赋值了(也就是初始化了),那么定义时可以不赋值
age: number = 18 // age没在constructor中初始化,故定义时,需要赋值,否则编译时会报错
constructor (name: string, age: number) {
this.name = name
}
}
还有种定义方式是直接在传参时加个访问修饰符 如:
class Person {
// 相当于定义了,并给他赋值了 this.name = name this.age = age
constructor (public name: string, public age: number) {
console.log(this.name) // 此时可以直接调用,因为已经定义了
console.log(this.age) // 此时可以直接调用,因为已经定义了
}
}
等价于下面
class Person {
name: string
age: number
constructor (name: string, age: number) {
this.name = name
this.age = age
}
}
ts中的类继承和es6中差不多,无非多了个类型限制也会被继承。我就不多说了,直接看代码
class Person {
name: string
age: number
constructor (name: string, age: number) {
this.name = name
this.age = age
}
say (): void {
console.log('你好啊,南昌老表')
}
}
class NanChang extends Person {
} // 通过extens实现继承,和es6一样
let chenMing = new NanChang('陈明', 18) // 类型限制也被继承了,因此,第一个参数必须传string, 第二个必须传 number
chenMing.say()
// 输出
你好啊,南昌老表
有一点需要注意,继承时,如果子类中也用了constructor构造器,那么必须先调用super() 方法,这点和es6是一样的。super()的作用就相当于是手动调用父类的constructor 构造器
可以看到,这个时候是会报错的,原因就是没有调用super()。下面是正确代码
class NanChang extends Person {
like: string
constructor(name: string, age: number, like: string) {
super(name, age)
this.like = like
}
}
let chenMing = new NanChang('陈明', 18, '看美女')
chenMing.say()
如果子类中存在和父类一眼的方法,那么会覆盖父类的方法,这点和es6是一样的,我就不再举例说明了。
通过在readonly关键字可以给属性添加只读特性。只读属性必须在声明时,或者在constructor构造函数中给他初始化值
可以看出,name属性是只读的,不可进行修改
ts中,类有一个存取器,分别是set() {} 和 get() {} 。这两个方法分别在属性值被设置时和被获取时被触发
这两个方法可以实现让我们在设置属性值和获取属性值时做一些事情,比如校验
class Person {
private _name = ''
get name ():string {
console.log('我的名字被获取了')
return this._name
}
set name (value: string) {
if (value.length > 5) {
throw new Error('名字不可大于5个字')
}
this._name = value
}
}
let p = new Person()
p.name = '陈明'
console.log(p.name) // 会分别输出 我的名字被获取了 和 陈明
p.name = '陈明说他今天很帅' // 会抛出错误
interface peoInterface{
name: string,
age: number,
isBoy: boolean,
work: string
}
function people (peo: peoInterface) {
console.log(peo.name)
console.log(peo.age)
}
people({
name: '陈明',
age: 18,
isBoy: true,
work: '程序员'
})
可以看出,参数peo 引用了接口 peoInterface,那么我传参时,就必须传和接口一样规范的对象。
属性必须有那4个,属性值的类型必须是接口定义的类型。如果不是就会报错,比如:
像上面那个例子,有些人有工作,有些人没工作,那我如果想让work这个属性变成可有可无的属性,也就是可选属性,只需要在属性后面加上一个?,那么这个属性就变成可选属性,那么这个属性可传可不传,不传也不会报错
可以看出,现在没有在报错了。
如果想让某个属性是只读属性呢,接口定义时,属性前加readonly关键字便可以了。
interface animal {
type: string,
readonly color: string
}
let dog: animal = {
type: '狗',
color: '白色'
}
dog.type = '猫' // 不会报错
dog.color = '黑色' // 会报错
当我们需要一个对象只校验接口中规定的某些字段,其余字段不进行校验时。这么说可能有点懵,我们来看个例子就懂了。
这里,我们并没有给interface添加 name和 address两个属性,所以会报错。但如果我们希望不对likcMusic和age之外的属性进行校验,那么我们只需加入一个额外校验的参数
interface people {
likeMusic: boolean,
age: number,
[propName: string]: any // []代表其他属性所有属性的集合 propName代表对象的key或者数组的索引,故只能为number或者string any表示其他所有属性值可为任意类型
}
let chenMing: people = {
likeMusic: true,
age: 18,
name: '陈明',
address: '南昌'
}
前面我们所看到的,基本都是应用于对象上的接口。现在我们来看看应用在函数上的接口是怎样的
interface addInter {
(a:number, b:number): number // 第一个number表示第一个参数类型,第二个表示第二个参数类型,第三个number表示函数返回值类型
}
// 表示声明一个符合addInter接口规范的函数
const count: addInter = function add (aaa: number, bbb: number) {
return aaa + bbb
}
听名字很明显,是一个约束某个类的接口。类要实现某个接口 需要用到 implements 关键字 。直接看代码吧
interface animalInter {
type: string,
color: string,
age: number,
say(str: string): void // 方法
}
// implements 表示实现的意思 代表某个类必须实现接口中定义的某些属性和方法
class animal implements animalInter {
type: string
color: string
age: number
constructor (type: string, color: string, age: number) {
this.type = type
this.color = color
this.age = age
}
say (str: string) {
console.log('汪汪汪')
}
}
接口继承接口
之前我们说过,类可以继承类。那么接口也是一样的,同样可以继承其他的接口。继承时,和类一样,通过extends关键字继承
interface inter1 {
name: string,
age: number
}
interface inter2 extends inter1 {
address: string
} // 此时inter2 就是一个具备了3个属性的接口了
// obj继承inter2 就必须有以上3个属性才行
let obj: inter2 = {
name: '陈明',
age: 18,
address: '南昌'
}
接口继承接口也可以同时继承多个 继承多个时,以逗号分隔 如:
interface inter1 {
name: string,
age: number
}
interface inter2 {
address: string
}
interface inter3 extends inter1, inter2{
like: string
} // 此时inter3同时继承了inter1和inter2 就是一个具备了4个属性的接口了
// obj继承inter3 就必须有以上4个属性才行
let obj: inter2 = {
name: '陈明',
age: 18,
address: '南昌',
like: '看美女'
}
接口继承类
顾名思义,接口同样可以继承一个类。当继承某个类时,就会继承这个类中所有的属性和方法。如:
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
say (str: string) {
console.log(str)
}
}
// 此时,peo接口继承了Person类,那么就继承了他的所有属性和方法(包括属性和方法的类型)
interface peo extends Person{
like: string
}
let people: peo = {
name: '陈明',
age: 18,
like: '看美女',
say: function (str) {
}
}
我们知道,当我们声明同名的变量时,后面的会覆盖前面的。但是对于接口而已,不会覆盖,而会叠加
interface aaa {
name: string
}
interface aaa {
age: number
}
// 以上两句话相当于下面一句话
// interface aaa {
// name: string,
// age: number
//}
let bbb: aaa = {
name: 'chenMing',
age: 18
}
但是接口合并需要注意,如果存在相同的属性时,那么两个接口必须属性类型也一致才能合并,否则会报错。如:
正确做法应该是
interface aaa {
name: string
}
interface aaa {
name: string,
age: number
}
typescript中有很多全局内置对象,不需要任何引入便可直接用的。
这里列出一些常用的
Date
Error
RegExp
HTMLElement
NodeList
let time: Date = new Date()
let reg: RegExp = /^a123/
let error: Error = new Error()
let dom: HTMLElement = document.getElementById('#app')
let eles: NodeList= document.querySelectAll('div')