一. 接口
接口是在面向对象编程中一种规范的定义,它定义了行为和动作的规范,起一种限制的作用。TypeScript 也有接口的概念,它被用来校验数据类型是否符合要求,就像是一份具有名称的契约或者规则,契约的内容规定了某个数据结构里面的数据组成和类型,只要有某处通过名称调用了这份契约,那就意味着此处的数据必须要接受并通过契约内容的检查,否则会报错。
在typescript中,接口通过interface来定义,只用来定义(数据)结构,不要去实现,当遇到一种复杂的数据的时候,我们可以通过接口来描述它的结构,下面我们将介绍四种接口:属性类接口,函数类型接口,可索引接口,类类型接口
属性类接口
属性类接口一般用作对于json对象的约束,对象也是一个复杂的数据,为了说明它的接口,我们要定义对象接口。
interface IFullName{
firstName:string;
secondName:string;
englishName?:string
}
function printName(name:IFullName){
console.log(name.firstName+'-----'+name.secondName)
}
printName({firstName:'LLL',secondName:'RRRR'})
注意
- 如果属性是可选的,直接在属性名称后面加? 如下
interface IFullName{
firstName:string;
secondName:string;
englishName?:string
}
-
如果我们想多传几个参数,但是想绕过接口中对于多于属性的检查,可以有以下三种方案
- 使用类型断言
printName({firstName:'LLL',secondName:'RRRR',age:12} as IFullName)- 使用索引签名
interface IFullName{ firstName:string, secondName:string, englishName?:string, [prop:string]:any }- 使用类型兼容性
let info={ firstName:'LLL', secondName:'RRRR', age:12 } printName(info)
函数类型接口
对方法传入参数以及返回值进行约束,我们要定义它们的类型结构,定义参数以及返回值的类型,不要定义函数体,因为函数体是函数的实现。
如果参数可有可无,后面添加? 如果函数有返回值,定义返回值类型 如果函数没有返回值,就是void。
interface CalcFn {
(n1: number, n2: number): number
}
function calc(num1: number, num2: number, calcFn: CalcFn) {
return calcFn(num1, num2)
}
const add: CalcFn = (num1, num2) => {
return num1 + num2
}
calc(20, 30, add)
当然,除非特别的情况,还是推荐大家使用类型别名来定义函数
type CalcFn = (n1: number, n2: number) => number
可索引接口
我们使用Interface来定义对象类型,这个时候其中的属性名、类型、方法都是确定的,但是有时候我们会遇到属性名都不确定的对象,此时可用可索引接口,如下:
interface IndexLanguage {
[index: number]: string
}
const frontLanguage: IndexLanguage = {
0: "HTML",
1: "CSS",
2: "JavaScript",
3: "Vue"
}
类类型接口
有时候,为了说明类的结构,我们也要定义类的接口,定义类的接口跟定义属性类接口一样
只定义属性和方法的结构,属性或者方法可有可无,后面添加?
interface Animal{
name:string;
eat(str:string):void;
}
class Cat implements Animal{
name:string
constructor(name:string){
this.name = name
}
eat(str:string):void{
console.log(`${name}吃${str}`)
}
}
let cat = new Cat('花猫')
cat.eat("鱼")
让一个class去实现一个interface,但是需要注意的是,接口描述的是类的公共部分,class还可以有自己单独的属性和方法 类接口通常首字母大写,并且使用类接口的时候,要让类使用implements关键字,来实现这个接口。
实现
声明类时,可以使用implements关键字指明该类满足某个接口。与其他显式类型注解一样,这是为类添加类型层面约束的一种便利方式。这么做能尽量保证类在实现上的正确性,防止错误出现不知具体原因。
interface Animal{
eat(food:string):void
sleep(hours:number):void
}
class Cat implements Animal{
eat(food:string){
console.log('cat is eating')
}
sleep(hours:number){
console.log('cat is sleeping')
}
}
注意:Cat 必须实现Animal声明的每个方法。一个类不限于只能实现一个接口,而是想实现多少都可以,如果忘记实现某个方法或属性,或者实现方式有误,Typescript会提醒你(不确定对不对)
与类型别名之间的比较
1. 类型别名更为通用,右边可以是任何类型,包括类型表达式(类型,外加&或|等类型运算符);而在接口声明中,右边必须为结构。例如下述类型别名不能使用接口重写:
type A=number
type B=A | string
2. 扩展接口时,Typescript将检查扩展的接口是否可赋值给被扩展的接口。例如:
interface A{
good(x:number):string
bad(x:number):string
}
interface B extends A{
good(x:string|number):string
bad(x:string):string
}
这样会报错
而使用交集类型时则不会出现这种问题,如下:
type A={
good(x:number):string
bad(x:number):string
}
type B=A & {
good(x:string|number):string
bad(x:string):string
}
这样TypeScript将尽自己所能,把扩展和被扩展的类型组合在一起,最终的结果是重载bad的签名,而不会抛出编译时错误
- 同一作用域中的多个同名接口将自动合并,同一作用域中的多个同名类型别名将导致编译时错误。
interface IPerson{
name:string
running:()=>void
}
interface IPerson{
age:number
}
let u:IPerson={
name:'llllll',
age:18,
running() {
console.log('running')
}
}
Typescript将自动把二者组合成一个接口 而使用类型别名时,将出现如下错误
与抽象类之间的关系
我们知道继承是多态的前提,所以在定义很多通用的调用接口时,我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式,但是父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,我们可以定义为抽象方法
抽象类: 只能被继承,不能被实例化的类
1. 抽象类和抽象方法一般用来定义某种标准,可以用来实现多态
2. 抽象类中可以有实例方法,但是必须包含一个抽象方法,否则没有意义
3. 抽象方法只能存在于抽象类中
4. 继承抽象类的子类,必须实现抽象类中定义的抽象方法
5. 抽象类无法被实例化 下面我们用案例来展示
abstract class Shape {
abstract getArea(): number
}
function makeArea(shape: Shape) {
return shape.getArea()
}
class Rectangle extends Shape {
private width: number
private height: number
constructor(width: number, height: number) {
super()
this.width = width
this.height = height
}
getArea() {
return this.width * this.height
}
}
class Circle extends Shape {
private r: number
constructor(r: number) {
super()
this.r = r
}
getArea() {
return this.r * this.r * 3.14
}
}
防止别人new一个类,定义成抽象类就不可以了 抽象类中的方法必须被子类实现
实现接口还是扩展抽象类?
实现接口其实和扩展抽象类差不多。区别是,接口更通用、更轻量、而抽象类的作用更具体、功能更丰富。
接口是对结构建模的方式。在值层面可表示对象、数组、函数、类或类的实体。接口不生成JavaScript代码,只存在于编译时。
抽象类只能对类建模,而且生成运行时代码,即JavaScript类。抽象类可以有构造方法,可以提供默认实现,还能为属性和方法设置访问修饰符。这些在接口中都做不到。
具体使用哪个,取决于实际用途。如果多个类共用同一个实现,使用抽象类。如果需要一种轻量的方式表示“这个类是T型”,使用接口
二. 泛型
泛型的本质是在类型层面施加约束的占位类型,这种类型可以用在类、接口和方法的创建中,是程序设计语言的一种风格和规范,在使用时作为参数指明类型。也就是说泛型在定义时并不知道具体是什么类型,而是在使用时由开发者来决定,这样定义一个泛型就可以支持任何类型了,这也是泛型的优点所在。
先看一个简单的例子:
function sum<T>(num: T): T {
return num
}
// 1.调用方式一: 明确的传入类型
sum<number>(20)
sum<{name: string}>({name: "LLL"})
sum<any[]>(["abc"])
// 2.调用方式二: 类型推到
sum(50)
sum("abc")
T 是类型变量,它是一种特殊的变量,只用于表示类型而不是值,使用 <> 定义。定义了类型变量之后,你在函数中任何需要指定类型的地方使用 T 都代表这一种类型,这样也能保证返回值的类型与传入参数的类型是相同的了
什么时候绑定泛型
声明泛型的位置不仅限定泛型的作用域,还决定TypeScript什么时候为泛型绑定具体的类型
以以下例子为例
type Filter={
<T>(array:T[],f:(item:T)=>boolean):T[]
}
//使用
let filter:Filter=(array,f)=>
注:以下所说的调用签名为TypeScript中表示函数类型的句法,他的句法与箭头函数十分相似,为 (a:number,b:number)=>number
这种情况T在调用签名中声明(位于签名的开始圆括号前面),TypeScript将在调用Filter类型的函数时为T绑定具体类型 而如果这样写:TypeScript则要求在使用Filter时显式绑定类型
type Filter<T>={
(array:T[],f:(item:T)=>boolean):T[]
}
即
let filter:Filter<number>=(array,f)=>
一般来说,TypeScript在使用泛型时为泛型绑定具体类型:对函数来说,在调用函数时,对类来说,在实例化类时;对类型别名来说和接口来说,在使用别名和实现接口时
泛型函数
我们可以定义一个泛型函数类型,泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面。
直接定义:
let getValue: <T>(arg: T) => T = function<T>(arg: T): T {
return arg
}
使用类型别名定义
type GetValue = <T>(arg: T) => T
let getValue: GetValue = function<T>(arg: T): T {
return arg
}
也可以使用完整型调用签名
type GetValue ={
<T>(arg: T):T
}
let getValue: GetValue = function<T>(arg: T): T {
return arg
}
完整型调用签名也可以使用以下这种方式
type GetValue<T> ={
(arg: T):T
}
let getValue: GetValue<number> = function<T>(arg: T): T {
return arg
}
注:将T的作用域限定在类型别名GetValue中,TypeScript则要求在使用GetValue时显式绑定类型
使用接口定义
interface GetValue{
<T>(arg: T): T
}
let getValue: GetValue = function<T>(arg: T): T {
return arg
}
// 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以
let getValue2: GetValue = function<U>(arg: U): U {
return arg
}
对于接口而言,我们可以把泛型参数当作整个接口的一个参数,这样我们就能清楚的知道使用的具体是哪个泛型类型。如下:
// 泛型变量作为接口的变量
interface GetValue<T>{
(arg: T): T
}
let getValue: GetValue<string> = function<T>(arg: T): T {
return arg
}
以上一共提供了六种定义泛型函数类型的方法,可视情况自行选择
泛型让函数的功能更具一般性,比接受具体类型的函数更强大。我们知道,把函数的参数注解为n:number,参数n的值就被约束为number类型。同样,泛型T把T所在位置的类型约束为T绑定的类型
泛型类
泛型类使用( <>)括起泛型类型,跟在类名后面。
class Sum<T> {
value: T;
add: (x: T, y: T) => T;
}
// T 为 number 类型
let test = new Sum<number>();
test.value = 0;
test.add = function(x, y) { return x + y; };
// T 为 string 类型
let test1 = new Sum<string>();
test1.value = "";
test1.add = function(x, y) { return x + y; };
类有两部分:静态部分和实例部分, 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
泛型约束
我们有时在操作某值的属性时,是事先知道它具有此属性的,但是编译器不知道,就比如有个例子,我们访问 arg.length 是行不通的:
function getValue<T>(arg: T): T {
console.log(arg.length) // 类型“T”上不存在属性“length”
return arg
}
现在我们可以通过泛型约束来对泛型变量进行约束,让它至少包含 length 这一属性,具体实现如下:
// 定义接口,接口规定必须有 length 这一属性
interface Lengthwise{
length: number
}
// 使用接口和 extends 关键字实现约束,此时 T 类型就必须包含 length 这一属性
function getValue<T extends Lengthwise>(arg: T): T {
console.log(arg.length) // 通过,因为被约束的 T 类型是包含 length 属性的
return arg
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
getValue(3) // 类型“3”的参数不能赋给类型“Lengthwise”的参数
getValue({value: 3, length:10}) // right
getValue([1, 2, 3]) // right
注意:不管是类型别名,还是接口 还是泛型,他们只和类型有关,与具体的值没有什么关系