一、TypeScript是什么
TypeScript(TS)
是JavaScript(JS)
的超集,TS
兼容JS
的所有功能,并额外提供了类型检查和基于类的面向对象功能。JS
是动态语言,只能在代码运行的时候检查类型错误(Type error
),遇到这种错误后,浏览器会抛出异常,并且停止执行后面的 JS
代码,严重降低用户体验。TS
是静态语言,可以在代码编写阶段检查类型错误,并且不允许改变变量类型,很大程度上避免了Type error
的发生。TS
实现了对 JS
的向下兼容,JS
代码可以不加修改地运行在TS
上。
二、TypeScript类型注解的概念
可以理解为类型声明,语法是:let varName: typeName
,为变量添加类型约束。类型注解了以后,就只能将注解类型的值赋予该变量,否则编译器会报错。
多类型注解。用竖线 “|
” 连接多种类型来注解变量,就可以使变量接受多种类型。
// 以下类型注解,表示变量a可以接受number和string两种类型数据的赋值
let a: number | string = ""
a = 2
console.log(a)
三、TypeScript的类型推断
TS
尽可能在确保不出错误的情况下,为开发人员减少代码输入。如果在声明类型的时候同时赋值了,TS
可以推断该变量的类型,从而可以忽略类型注解。一般在两个地方可以出现类型推断:
- 声明变量并立即赋值时;
- 函数返回数据时。
四、TypeScript的类型
TypeScript
包括以下两种类型:
(一)JS中已有的基础类型
-
number
、string
、boolean
、null
、undefined
、symbol
最常用的就是
number
、string
和boolean
类型。默认情况下,null
和undefined
是所有类型的子类型,可以赋值给number
、string
和boolean
类型。但是如果指定了--strictNullChecks
标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。 -
array
、object
、function
(二)TS新增类型
联合类型、自定义类型(类型别名)、接口、元组、字面量、枚举、void
、any
五、TypeScript类型注解语法
Typescript
中实现类型保护的机制,就是通过类型注解语法实现的。它的基本语法结构,就是在需要注解的变量、对象、属性等名称后面用英文冒号:
引出注解语句。
(一)原始类型
-
number
、string
、boolean
、null
、undefined
、symbol
类型变量,在变量声明的时候,在变量名后直接加类型,**注意首字母是小写的。**
// 在变量声明的名称后面,用冒号语法,后面再跟上类型 let a**:** string = "Hello,TypeScript" a = 0 // 编译器将报错:不能将 0 分配给 string // 以下正确 let b**:** number = 2 let isLoading**:** boolean = true
(二)数组类型
-
单一类型的注解。表示该数组内所有元素只接受一种数据的赋值。
let arr1: number[] = [0,2,3] let arr2: string[] = ['0','00','000'] // 另外一种注解方式,实际上是泛型接口。TS 将数组用泛型接口实现了。 let arr3: Array<string> = ['ddd']
-
联合类型注解。表示该数组元素可以接受多种类型。
let arr1: (number | string | null)[] = [1, "2", null] arr1 = [33, "ddd", null, null] console.log(arr1)
(三)类型别名(自定义类型)
对于比较复杂而且多次使用的类型注解,为他起一个别名,便于记忆和书写。使用**type**
关键字来创建类型别名,或者说自定义一个类型。
// 1. 定义一个类型
type userDefine = (number | string | null)[]
// 2. 然后用这个类型去注解一个变量
const var1: userDefine = [1, "222", null]
console.log(var1)
type num = 1 extends number?1:0 这个如何理解?在 type 中,extends 的意思是,前者是否属于后者,也就是说,前者是否可以赋值给后者。因此这句话可以断句为:type num = (1 extends number)?1:0
(四)函数类型
函数主要通过参数和返回值的类型类定义类型。有两种定义方式:
-
单独定义参数和返回值类型。参数的类型注解直接用冒号跟在参数后面,返回值用冒号跟在参数和函数体之间
// 函数声明的方式 function f(a: number, b: string): string { console.log(a, b) return "sss" } // 函数表达式的方式 const f = (a:number,b: string): string => { console.log(a,b) return "ssss" }
-
同时指定参数和返回值类型。当使用表达式的方式定义函数的时候,可以利用箭头函数的形式定义函数参数类型和返回值类型。
const f2: (a: number, b: number) => number = (a, b) => { return a + b // 此处虽然是单行,但仍然要显示调用return,不然类型不匹配。 } f2(3, 4)
-
没有返回值的函数类型。没有返回值的函数,可以显示说明为void,或者不写
// void返回类型 function f3(): void { console.log("hello") } // 或者 function f3() { console.log("hello") } f3()
-
可选参数。有的函数的参数,可以指定,也可以不指定,不强制必须赋值。在参数名后加上?
function f4(a:number,b?:number):number{ return a + b }
-
利用箭头函数定义函数类型。这定义了一种函数类型,但没有实现该函数体。参数类型跟定义箭头函数实例一样,但
**返回值类型的位置不一样,直接跟在箭头后面。**
TS 遇到:
就知道后面的代码是写类型用的。// 定义一个函数类型 type1,它有一个数值类型参数,返回值是数值类型 type type1 = (a: number) => number // 实现这个函数类型的实例 const ftype: type1 = (ax: number) => { console.log(ax); return 1}
-
对于默认参数,传递undefined或者不传递数据,
TS
会采用函数定义时候指定的默认数据。看下面的例子:function add(a:number,b:number=3){ console.log(a+b) return a+b } add(1,2) // OK,结果为:3 add(1) // OK,结果为:4 add(1,undefined) // OK,结果为4
(五)对象类型
-
作用。用来描述对象的结构声明
-
方法如下:将对象每个属性像注解原始类型一样,两个属性用分号隔开,与声明变量的逗号不一样。如果用回车分开,就可以不用分号。
-
对象也可以定义可选属性,方法是在属性名前加”?“。
// 用分号分隔: let person: {name: string; age: number} // 用回车分隔: let person2: { name: string age: number } // 定义可选属性 let person3: { name: string, age: number, sex?: string } // 定义的时候立即赋值,这个时候实际上可以不用写类型注解,ts 能够自动推断出来 let person4: { name: string; age: number } = { name: '张', age: 24 }
(六)接口
-
接口的概念。类似于简单变量的
type
,对于相同对象类型多次使用的场景,可以用interface
来定义一个接口,达到简化代码的目的。 -
用法。
interface name {…;….}
,注意与类型别名的声明方法不同,这里不用“=
”。 -
接口里也可以定义可选属性。
-
接口里也可以有方法,要定义一个函数类型,明确参数和返回值类型。
-
接口的内的属性和方法是一种类型声明。**不能提供默认值,**否则
TS
会把默认值当做一种类型声明(字面量类型),不可更改。 -
接口的声明不能用
=
。 -
接口可以继承,也就是在一个接口上扩展属性。
// 定义一个接口 interface person5 { name: string age: number sex?: string // 以下两种不推荐,TS 会认为将 gender 和 grade 声明为'男'和 '1'类型。 gender:'男', grade: 1, } // 实现一个接口实例 let p:person5 = {name: '哈哈哈',age:44,sex:'mail'} // 继承一个接口 interface person6 extends person5 { // 在 person5的基础上增加一个属性 grade: number } // 实现继承接口的实例 let p2: person6 = { name: 'name', age: 444, sex: '男', grade: 4 }
-
用接口定义函数类型
// 1. 定义函数类型接口 interface IFn { (p:string):string } // 2. 用函数类型接口实现函数 // 实现函数类型 let fn1:IFn = (b:string)=>''
-
接口的函数属性
接口与类不一样,只有方法属性。但也可以用两种方式来定义属性的类型,返回值的定义方式的区别,即:和=>。
// 1. 用量方法定义接口的属性 interface IFn2 { fn: () => string fn2():void } // 2. 以上两种方式声明了两个方法属性,而不是类的成员函数 // 3. 因此尽管以下声明方法的方式不一样,但实现的方法是一样的 // 以下是第一种方法: let fn2imp: IFn2 = { fn: () => '', fn2: ()=> {} } // 以下是第二种方法: let fn2imp2: IFn2 = { fn() {return ''}, fn2(){} }
(七)元组
-
概念。用来明确定义一个数组,包括它的元素个数,和每个元素的类型。他是一种特殊的数组,也可以理解为固定长度、固定元素类型的数组。
-
用法。在类型注解部分直接用方括号。元组成员必须全部具有或全部不具有名称。
-
也可以定义一个元组为类型别名。
// 声明一个元组 let xy: [x: number, y: number] = [3, 4] // 赋值的时候必须严格遵守个数和类型的限制 xy = [4,4] // 可以定义一个元组的类型别名吗? type xy2 = [x: number, y: number] let xy3:xy2 = [3,4]
(八)类型断言
-
概念。一般获取变量的时候,可能会获得比较泛型的父类型,通过
as
关键字,可以指定一个更具体的子类型,从而访问到子类型特有的方法和属性。 -
用法。
as
和<>
操作符。const aLink = document.getElementById(’link’) as HTMLAnchorElement // 或者 const aLink = <HTMLAnchorElement>document.getElementById(’link’) aLink.href='ddddd'
-
类型断言只是欺骗编译器不报错,但代码运行后可能会出现无法预料的结果。因此,除非十分有把握,尽量少用类型断言。例如以下示例。
type p = number | string function showp(P: p) { console.log((P as string).length) } showp(2) // 输出 undefined,不是所期望的结果。
(九)字面量类型
-
概念。一个常量,例如一个字符串、一个数字,实际上是一个字面量。例如,用 const 定义了一个变量,并赋值,实际上就是定义了一个字面量。
const t = ‘b-737’,t 就是b-737类型的字面量。
-
可以通过联合类型将几种字面量结合起来,定义一个类型。从而是该变量的意义更为精确明确易懂。
// 以下定义的函数的参数,只能选择以下四种字符串。 function changeDirection(direction: 'up' | 'down' | 'left' | 'right') // 定义一种特殊类型,表示性别 type Sex = 'mail' | 'femail'
(十)枚举类型
-
概念。类似于字面量与联合类型组合的功能。两者可以完全替代。
-
定义方法。用
enum
关键字声明:enum name {var1,var2}
,内部常量不需要引号,因为他相当于属性名,它有值,默认从 0 开始自增。 -
枚举成员的值还可以为字符串。还可以混合。字符串枚举没有自增长行为。定义字符串枚举的时候,必须初始化。
-
从左边开始的连续几个枚举成员可以不初始化,系统会自动给他赋值。
-
使用方法。枚举定义完成后,可以直接使用,无法使用 let 或 const 进行再赋值。
enum direction { left=10,right=20,up=30,down=33,s="name" } console.log(direction.s) enum enum1{ l,r='4' }
(十一)any类型
他可以接受任何类型。不建议使用这种类型,会失去类型保护功能。any 类型的数据可以赋值给任何类型。
(十二)typeof 操作符
-
概念。根据类型的上下文环境,来获取变量或属性的类型。通过这个操作符来动态查询一个对象的类型,然后用他来做类型标注,简化操作。
-
注意。他只能获取变量或者对象属性的类型。
// 1. 先定一个对象 let p = {x:1 ,y: 2} // 2. 用 typeof得到 p 的类型,用来定义 function f(point: typeof p){} f({x:3,y:3}
六、TypeScript 的高级类型
(一)class 类
-
类的声明和实例化。
class man{ } const people = new man()
-
类的属性定义。既可以带默认值,也可以不带。两个属性之间,用分号分隔,也可以用回车分开。
-
类的成员也可以是可选的;
-
如果声明类成员的类型,用
:
,如果初始化类成员,则需要用=
,包括初始化函数成员。class teacher { age: number // 没有默认值。用回车分隔 gender = '男' // 初始化默认值注意这里的等于号,不是冒号。后期可以更改。 score?:number // score 是可选属性 }
-
类的构造函数。用 constructor 关键字来定义。他很像一个函数。构造函数没有返回值。
// 声明一个类 class teacher { age: number; // 没有默认值 gender = '男'; // 有默认值。注意这里的等于号,不是冒号。后期可以更改。 constructor(age: number,gender: string){ this.age = age this.gender = gender } } // 用构造函数实例化 const t1 = new teacher(44,'男')
-
类的实例方法。定义方法与对象的方法一样。
-
类的继承。可以实现一些公共属性和方法的复用。有两个方法继承类。一种是使用
extend
关键字,从父类继承方法和属性,就像接口继承一样。二是通过实现接口implements
的方式来,必须实现接口中的所有属性和方法。// 通过一个实现接口来继承一个类,要实现接口的所有属性和方法 // 也可以在实现的时候增加方法和属性 interface animal{ // 声明了一个函数类型的属性 move move(): void, age:number } class dog implements animal { move() { } age=10 // 增加一个属性 hight=100 // 增加一个方法 move(){} }
-
类成员的可见性。默认情况下,所有属性和方法对外是可见的。即可以通过
.
运算符访问他们。可见性有三种:public
、private
、protected
。public
是默认的属性,在任何场合都可以访问到它。protected
在当前类的其他方法
和子类的方法
中可见(就是在定义该类的代码区域),但在所有实例对象中不可见。private
是私有属性,只能在当前类中可见,在子类和实例对象中均不可见。static
静态成员。类的常量,实例不能访问他。在当前类和子类的方法中可以访问它。
-
readonly
修饰符。只有构造函数可以修改它。防止构造函数以外
对属性进行赋值。它只能修饰属性,不能修饰方法。还可以用在接口、对象类型中。 -
定义类的时候,所有属性和方法都要初始化,**
新版编译器
**在编译阶段会出错。要不初始赋值,要不在构造函数内赋值。
(二)抽象类
所谓抽象类,是指只能被继承,但不能被实例化的类,就这么简单。抽象类用一个 abstract
关键字来定义。抽象类有两个特点:
-
抽象类不允许被实例化。
-
抽象类中的抽象方法必须被子类实现。
abstract class CPerson { name=''; // 注意观察两种定义方法类型的方法,性质不一样,在继承类中实现的方法也不一样 **abstract fn(): void; // 这是成员函数** **abstract fn2:()=>void // 这是成员属性** } //const p1 = new CPerson; // Error。抽象类不能直接实例化。 class CMan extends CPerson { // OK.抽象类只能被继承 // 必须实现抽象类的 abstract 成员 constructor() { super() } // 成员方法的实现方式,要用到**声明**方式 fn(): void { } // 成员属性的实现方式,要用**赋值**的方式 fn2 = () => { } }
(三)类型兼容性
-
类的兼容性。如果两个类的成员结构完全一样,则认为他们两个是同一类型。属性名称和类型必须一样,属性值和方法体可以不一样。如果一个类
A
包含了另一个类B
的所有属性和方法,那么可以认为A
兼容B
,成员多的可以赋值给成员少的:class a { b: number; c() { }; } class b { a: number; b: number; c(){} } // class b 的属性完全包含 a,所以可以兼容 a let a1: a = new b; // 以上代码在编码阶段,不会收到错误提示。但编译后会报错。因为有成员没有初始化。做如下修改: // class类型兼容性的演示 class a { b: number = 1; c() { }; } class b { a: number = 44; b: number =1 ; c(){console.log('d')} } // class b 的属性完全包含 a,所以可以兼容 a let a1: a = new b; console.log(a1) // 打印 a1 的结果如下:b { a: 44, b: 1 },类型 b 的值赋值给了 a,多的没有接受。
-
接口的兼容性。类似于类的兼容性,并且两者之间可以兼容。看以下代码:
interface IF1 { name: string age: number } // 以上代码定义了接口IF1。使用两种方法使用它。 // 第一种方法,等于号赋值,必须完全一致,属性的个数和类型必须完全一致。 let person1: IF1 = { name: '', age: 4 } // 第二种方法:定义一个带IF1型接口参数的函数。 function iffunc(p: IF1) { } // 单独定义一个对象,没有指定类型,这个包含了IF1参数的所有属性,并且多了一个height属性 let pp = { name: '', age: 3, heigh: 3 } // 把他作为参数传递给函数,没有问题。 iffunc(pp) // OK // 直接传递一模一样的字面量对象,也是没问题的 iffunc({ name: '', age: 10 }) // 但是在字面量对象中增加一个属性,就不行了 iffunc({ name: '', age: 10 ,height:30}) // 但是,如果像这样调用,就会出错:Argument of type '{ name: string; age: number; heigh: number; }' is not assignable to parameter of type 'IF1'. Object literal may only specify known properties, and 'heigh' does not exist in type 'IF1'. iffunc({ name: '', age: 3, heigh: 3 }) // Error
什么原因呢?网上找的解释:
原来的变量的约束外属性还是可以被其他代码使用的, 但是如果是literal的话就因为完全不会被其他地方使用而让额外属性完全没有意义.
因此,可以这样理解:第二种函数调用方法,其实也是一种赋值。官方的解释是:
对象字面量会被特殊对待而且会经过_额外属性检查_,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
绕开这些检查非常简单。 最简便的方法是使用类型断言:
iffunc(<IF1>{ name: '', age: 3, heigh: 3 }) // OK
-
函数参数的兼容性。两个函数,在返回值一样的情况下,如果一个函数
a
的参数完全包含另一个函数b
的所有参数,那么b
可以兼容a
,也就是说函数a可以赋值给b
。参数少的可以赋值给参数多的。
(四)type和interface的异同
1. 相同点
- 都可以定义一个对象或函数
- 都允许继承。
type
的继承方式是交叉类型,用&
;interface
的继承方式是用extends
。
2. 不同点
interface
(接口) 是TS
设计出来用于定义对象类型的,可以对对象的形状进行描述。type
是类型别名,用于给各种类型定义别名,让TS
写起来更简洁、清晰。type
可以声明基本类型、联合类型、交叉类型、元组,interface
不行。interface
可以合并重复声明,type
不行。
七、泛型
(一)泛型的概念和定义方法
-
泛型中的 T 就像一个占位符、或者说一个变量,也可以理解为
类型变量
、动态类型
。在定义使用泛型的类型的时候,把后期希望动态变化的类型用泛型类标注。在使用的时候,把需要使用的类型像参数一样传入,它可以原封不动地在使用该类型变量(泛型)的地方自动替换成需要的类型。也就是说,对需要固定的类型暂时不指定,一旦程序运行起来后就会自动固定。提供了较强的灵活性。 -
泛型的语法是
<>
里写类型参数,一般可以用T
来表示,放在函数名、类型名和接口名之后。如下例,在函数名后面的<t>
声明了泛型类型名t
,然后在参数、返回值等处应用了他。 -
对于一些对于多种类型数据都可以通用的功能,可以用泛型来定义一个类型变量,在实际使用该功能的时候再来确定泛型的具体类型,从而实现了类型的灵活性,又确保的类型的保护,提高功能的数据安全性和灵活性,实现功能的灵活复用。
-
泛型是可以在代码库中定义和重复使用的代码模板。 它们提供了一种方法,可用于指示函数、类或接口在调用时要使用的类型。 可以通过将参数传递给函数的方式来理解,不同之处是使用泛型可以指示组件在被调用时应该使用哪种类型。
// 泛型声明一个类型变量,等待用户给他指定具体类型,他接受这个类型。 // 1. 箭头函数的形式声明泛型函数,将类型变量放在参数前面 const f1 = <T> (b:T):T => { return b } // 2. 普通声明方式,将类型变量放在函数名后面 function f2<T>(a: T) { return a }
-
定义泛型的时候,也可以提供默认值,例如以下代码:
// 定义以下接口的时候,为泛型 T提供了默认类型 interface CPerson<T=number>{ name: string, age:T }
(二)泛型的调用
调用泛型的时候,需要指定或者被 TS 自动推断出泛型的具体类型。例如以下函数的调用:
-
既可以使用
print<number>
的方法显示指定泛型的具体类型; -
也可以忽略具体类型的指定,通过传递合法的参数,让
TS
自动推断出泛型t的
具体类型为number
。一般情况下,可以省略这个具体类型指定。// 以下函数中,arg1 的类型可以随便给,arg2只能是 number function print<t>(arg1: t, arg2: number): t { console.log(`${arg1},${arg2}`) // 由于在函数声明的时候,返回值为泛型t,所以只能返回 arg1 类型的 return arg1 } // 以下调用语句,指定了泛型类型为 number,那么第一个参数必须为 number 类型,否则会报错。 print<number>(4,3) // 以下语句中,r 的类型会随着 arg1 的类型而推论确定 let r = print(2, 2)
这样,我们就做到了输入和输出的类型统一,且可以输入输出任何类型。如果类型不统一,就会报错。
(三)泛型约束
-
依靠外部约束泛型
由于泛型的千变万化,所以在实际使用中有时候会因为这个变化带来烦恼。例如以下代码,有时候希望这个变量是有
length
这个属性的,但TS
会报错,因为泛型的原因,不可能保证所有参数都有length
属性。// 程序员想以字符串的形式调用这个参数的时候,会报错: function f1<T>(a:T){ return a.length }
解决这个问题,用类型变量的收窄,可以用
interface
来解决:interface ILength {length:number} // 此处的extends是用来约束 T 的类型,不是继承的意思 function f3<T extends ILength>(c:T):number { return c.length }
-
泛型之间约束
例如以下需求,需要获取某个变量的属性,
keyof
关键字得到了T
的所有属性名的联合类型,例如以下案例,keyof
得到一个联合类型是:name|age
function getProp<T, key extends keyof T>(p:T,k:key){ return p[k] } const prop1 = { name: '张景平', age: 43 } getProp(prop1, 'name') getProp(prop1,'age')
(四)接口泛型
- 声明方法。在接口名后声明泛型,在泛型内部就可以使用它。
interface IPerson<T>{
a: T,
b: T
}
// 也可以进行泛型约束
interface IPerson<T,U extends keyof T>{
a: T,
b: T,
c: U
}
- 使用方法。与其他泛型不同,例如使用了泛型的函数,使用泛型接口的时候,**必须显示指定泛型的类型,因为接口没有类型推断功能。**以下是定义和调用泛型接口的示例::
// 以下定义了一个泛型接口
interface IPerson<T,U extends keyof T>{
a: T,
b: T,
c: U
}
// 以下两处使用泛型接口,都要显示指定泛型的类型
function f4(b: IPerson<string,number>) {
}
// 显示指定
let IPerson1: IPerson<string, number> = {
a: 'heelo',
b: 'heell',
c: 3
}
(五)类型泛型
-
声明方法。在类名后加
<>
,类就成了泛型类; -
使用方法。与泛型接口类似,一般情况下,在使用泛型类的时候也要显示指定泛型的类型。
-
具体使用泛型类的时候可以省略具体类型指定的情况。如果有构造函数,且构造函数的参数使用了泛型,那么可以在给构造函数传递参数的情况下,不用显示指定泛型的类型。
// 定义一个泛型类,将泛型放在类名后面 class cla<T>{ a: T; b: T c = 8 d = "string" e: (b: T) => T constructor(a:T,b:T) { this.a = a this.b = b this.e = (b:T) => b } } const cla1 = new cla(1,3); cla1.e(1)
(六)签名、映射
-
签名属性。表示不知道接口的属性有多少个,提供一个占位符。
// 使用单一类型 interface IKey<t> { [index:number]:t } const s:IKey<string> = "ddd" // 使用联合类型 interface IKey<t> { [index:number]:t|number } const s:IKey<string> = [0,3,3,'33']
-
在 type 声明中,可以使用[]查询对象属性的类型。