前言 TypeScript是微软开发的基于JavaScript(JS)的一个扩展语言,包含JS所有内容,并增加了静态类型检查、接口、泛型等特性,适合大型项目开发。
为什么需要TypeScript?主要是因为JavaScript有如下困扰:
本身是动态数据类型,导致数据类型不清晰
无法判断是否有逻辑漏洞
可以访问不存在的属性
无法发现低级的拼写错误
TypeScript是JavaScript的一个超集,主要用来进行代码的静态类型检查(在运行或编译前就发现并提出错误),这在大型项目中会很有用。
Python的TypingHit的灵感或许来自这里
编译TS 手动编译 浏览器等运行环境无法直接运行TypeScript,需要先将Ts转化为Js
安装TypeScript
新建index.ts文件
1 2 3 let str : string = "hello world" ;console .log (str);
在终端运行
tsc为TypeScript Compiler(ts编译器)的缩写
在文件夹会生成一个同名的index.js文件
1 2 var str = "hello world" ;console .log (str);
自动编译 可以启用自动监听
1 2 tsc --init tsc --watch [路径名/文件名]
tsc --init
会自动生成一个tsconfig.json
文件,文件中的选项都是ts转化js的配置项。tsc --watch
的作用是监听所在的路径是否有ts文件,如有则自动编译为js
常用的为下面三个属性
“strict”: true 是否开启严格模式
“target”: “es2016” 使用那种规范进行转化 es2016是指es7 也可以直接写ES7
“noEmitOnError”: true 当ts文件有语法错误的是否是否编译
“module”: “ES6” 指定ts使用的模块化规范 如”commonjs”,”ES6”等
快速上手 ts的基本用法即为为变量添加类型申明。
编写如下代码:
1 2 3 4 5 6 7 8 9 10 let userName : string = "hello world" ;let age : number = 20 ;let isMan : boolean = true ;function add (a: number , b: number ): number { return a + b; } let result : number = add (1 , 2 ); console .log (result);
编译得到的js如下
1 2 3 4 5 6 7 8 9 "use strict" ;let userName = "hello world" ;let age = 20 ;let isMan = true ;function add (a, b ) { return a + b; } let result = add (1 , 2 );console .log (result);
ts存在默认的类型推断
如:
1 2 let num = 0 ;num = "hello"
ts语法检查器默认将num指定为number类型,因此上述代码会出现“不能将类型“string”分配给类型“number”的报错
类型总览 支持的类型 JavaScript有如下数据类型
string
number
boolean
null
undefined
bigint
symbol
object (包含 Array Function Date Error等)
TypeScript有如下数据类型:
支持所有JavaScript类型
六个独有类型
any
unkown
never
void
tuple
enum
两个用于自定义类型的方式
大小写的区别 需要注意基本元素类型(小写)和包装器对象(大写)的区别:
1 2 3 4 5 6 7 8 9 10 11 let str1 : string ;let str2 : String ;str1 = "Hello" ; str2 = "Hello" ; str2 = new String ("World" ); console .log (typeof str1); console .log (typeof str2);
报错为不能将类型“String”分配给类型“string”。“string”是基元,但“String”是包装器对象。如可能首选使用“string”
。
报错显示String
表示的是包装器对象,而string
代表的是基元,因此出现了语法错误。ts官方推荐使用小写的类型申明
原始类型:它们内存占用少,处理速度快
包装对象:复杂类型,内存占用更多,在实际代码中很少使用
自动装箱:JavaScript在必要时会讲原始类型转化为包装对象,方便调用方法和属性(解释器自动完成的)
一个自动装箱的例子:
1 2 3 4 5 6 7 8 9 10 11 12 let str = "hello" ;let size = (function ( ) { let tempStringObject = new String (str); let lengthValue = tempStringObject.length ; return lengthValue; })(); console .log (size);
常用类型 any any
表示当前的变量可以是任意的数据类型,一旦赋予any
属性,就代表完全放弃了类型检查
1 2 3 4 5 6 7 8 9 let a : any ;a = "Hello" ; a = 10 ; a = true ; let b;
注意点:any
类型可以覆盖任何数据类型,这会导致类型原本规定的类型改变
1 2 3 4 5 6 7 8 9 10 11 let a : any ;a = "Hello" ; a = 10 ; a = true ; let b : string ;b = a; console .log (b); console .log (typeof b);
另外,any
类型可以调用任何属性与方法(即便本身类型不存在)
1 2 3 4 5 6 7 8 9 let a : any ;a = 9 ; a.toUpperCase (); a.length (); a.eee (); a.ppp ();
非迫不得已,不要使用any
类型
unknown unkown
是一个类型更加安全的any
,也表示当前数据类型可以是任意的值
1 2 3 4 5 6 7 8 let a : unknown ;a = "Hello" ; a = 10 ; a = true ; let b : string ;b = a;
从上面的实例可以看出unkown
不会出现把原本变量类型覆盖的情况(直接报错)。但是也会因此出现类型相同但是无法赋值的情况:
1 2 3 4 5 6 7 8 9 let a : unknown ;a = 9 ; a = true ; a = "hello" ; console .log (a); let b : string = a;
可以有三种方法解决这种问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let a : unknown ;a = "hello" ; let b : string ;if (typeof a === "string" ) { b = a; } b = a as string ; b = <string >a;
在unkown
中,不能调用任何属性和方法
1 2 3 4 5 6 7 let a :unknown ;a = "hello" ; a.toUpperCase (); a.trim (); a.length ;
同理可以使用断言来告诉编译器变量的类型
1 2 3 4 5 6 let a : unknown ;a = "hello" ; console .log (a); (<string >a).toUpperCase (); (a as string ).toUpperCase ();
never never
表示永远不会返回结果
不要将变量定义为never
,这将导致对象无法被赋予任何值,而且这也没有意义
1 2 3 4 5 let a :never ;a = 99 ; a = "hello" ; a = true ;
never
一般用于表示函数永远不会正常结束,如下三种例子:抛出异常、无限循环(无法结束)、自我无限调用
1 2 3 4 5 6 7 8 9 10 11 12 function demo ( ): never { throw new Error ("Error" ); } function demo2 ( ): never { while (true ) { } } function demo3 ( ): never { return demo3 (); }
never
一般是编译器自动推断出来的
1 2 3 4 5 6 7 let str :string = 'Hello' if (typeof str === 'string' ) { console .log (str.toUpperCase ()) } else { console .log (str); }
上述代码,else
的部分永远无法到达,所以会被自动推断为never
类型
void void
表示值为空,函数调用者不需要关心返回值,无法使用返回值做任何操作
常用于表示函数并没有返回值,且在形式上只接受undefined
作为返回值
1 2 3 4 5 6 7 8 9 10 11 function demo ( ) :void { } function demo2 ( ) :void { return undefined ; } function demo3 ( ) :void { return ; }
另外调用者不能根据其返回值做任何操作
1 2 3 4 5 6 7 8 9 function demo ( ): void { return undefined ; } let result = demo ();if (result){ }
这点和将返回值定义为undefined
是有明显的不同的
1 2 3 4 5 6 7 8 9 function demo ( ): undefined { return undefined ; } let result : undefined = demo ();if (result) { console .log ("result is not undefined" ); }
如果将函数返回值定义为void
,那么:
语法上:函数可以返回undefined
,无论是显示返回,还是隐式返回
语义上:函数调用者无需关心函数的返回值,也不应依赖返回值进行任何操作
object 基本用法 object 表示一个对象,他可以是基元之外的任意数据类型。
Object 在object的基础上可以表示基元,它们都不能表示null
或者undefined
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 let a : object ;a = { name : "Max" }; a = {}; a = function ( ) {}; a = []; let b : Object ;b = { name : "Max" }; b = {}; b = function ( ) {}; b = []; b = 1 ; b = true ; b = "string" ;
从上述代码可以看出,object表示的范围过于宽泛,因此在实际中并不使用
替代用法 开发中object并不常用,那么我们如何定义函数、对象、数组的类型呢?
函数
基础的用法如下所示
1 2 3 4 5 6 7 let add : (a: number , b: number ) => number ;add = function (a, b ) { return a + b; }; add = (a, b ) => a + b;
另外还可以使用接口,自定义类型等方式进行定义
对象
基础的用法如下
1 2 3 4 5 6 7 8 9 10 11 let person : { name : string ; age : number ; [key : string ]: any ; }; person = { name : "gcnanmu" , age : 30 , gender : "man" , };
其中的key
其实只是个标识符,换什么都可以
数组
基础用法如下:
1 2 3 4 5 6 7 8 9 let arr : string [];let arr2 : Array <string >;arr = ["a" , "b" , "c" ]; arr2 = ["a" , "b" , "c" ];
Array<string>
属于泛型
tuple tuple类型并非一个关键字,它只是一种形式 ,可以理解为一种特殊的数组类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 "use strict" ;let tuple;let tuple2;let tuple3; tuple = [1 , "hello" ]; tuple2 = [1 , "hello" ]; tuple2 = [1 ]; tuple3 = [1 , "hello" ]; tuple3 = [1 , "hello" , "world" ]; tuple3 = [1 ]; tuple3 = [1 , "hello" , "world" , "foo" ]; console .log (typeof tuple, typeof tuple2, typeof tuple3);
enum 困境 enum表示枚举类型,用于定义一组命名的常量 ,能增强代码的可读性,让代码更好维护。
常见的困境如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function getDirection (direction: string ): string { switch (direction) { case "Up" : return "向上" ; case "Down" : return "向下" ; case "Left" : return "向左" ; case "Right" : return "向右" ; default : return "其他方向" ; } }
以上代码虽然逻辑上是没有问题的,但是无法防止出现写错的隐患,且代码的可读性也比较差。可以通过枚举类型将所有方向信息装入一个类型中。
数字枚举 1 2 3 4 5 6 7 8 enum Direction { Up , Down , Left , Right , } console .log (Direction );
打印得到的结果为:
1 2 3 4 5 6 7 8 9 10 { '0 ': 'Up', '1 ': 'Down', '2 ': 'Left', '3 ': 'Right', Up: 0 , Down: 1 , Left: 2 , Right: 3 }
从上述输出可以看出Direction是一个对象,且标号从0开始递增,且具有反身性。
定义好Direction后,我们就可以修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 enum Direction { Up , Down , Left , Right , } function getDirection (direction: Direction ): string { switch (direction) { case Direction .Up : return "向上" ; case Direction .Down : return "向下" ; case Direction .Left : return "向左" ; case Direction .Right : return "向右" ; default : return "其他方向" ; } }
这样改写后的好处在于代码的可读性变高,且写代码的时候会出现选择项(只能从定义的类型中选择,无其他选项),可以杜绝写错的情况。
如果要更改递增的开始数字,可以修改第一个变量的赋值。
1 2 3 4 5 6 7 8 enum Direction { Up = 10 , Down , Left , Right , } console .log (Direction )
得到的打印结果为
1 2 3 4 5 6 7 8 9 10 { '10 ': 'Up', '11 ': 'Down', '12 ': 'Left', '13 ': 'Right', Up: 10 , Down: 11 , Left: 12 , Right: 13 }
实际上,我们无需关心它实际代表的取值
字符串枚举 enum中的变量值可以不用number,使用string
1 2 3 4 5 6 7 8 enum Direction { Up = "Up" , Down = "Down" , Left = "Left" , Right = "Right" , } console .log (Direction )
打印得到的结果如下
1 2 3 4 5 6 enum Direction { Up = "Up" , Down = "Down" , Left = "Left" , Right = "Right" , }
从输出可以看出,这样做之后就不再具有反身性
常量枚举 如果基本的用法来说,编译得到的js可能会出现很多冗余的代码
1 2 3 4 5 6 7 8 enum Direction { Up = "Up" , Down = "Down" , Left = "Left" , Right = "Right" , } console .log (Direction .Up );
得到的js如下
1 2 3 4 5 6 7 8 9 "use strict" ;var Direction ;(function (Direction ) { Direction ["Up" ] = "Up" ; Direction ["Down" ] = "Down" ; Direction ["Left" ] = "Left" ; Direction ["Right" ] = "Right" ; })(Direction || (Direction = {})); console .log (Direction .Up );
在js中我们得到了整个枚举类型的转意,但是实际上,我们只使用了Direction.Up
这一个属性,那么其他信息就是冗余的。
改进的方法就是在enum前面添加一个const关键字
1 2 3 4 5 6 7 8 const enum Direction { Up = "Up" , Down = "Down" , Left = "Left" , Right = "Right" , } console .log (Direction .Up );
编译得到的js就会删除多余的冗余部分(会留下提示),显著降低代码量
1 2 "use strict" ;console .log ("Up" );
官方的描述:常量枚举是一种特殊的枚举类型,他使用const关键字定义,在编译时会被内联,避免生成一些额外的代码
何为编译时的内联?
TypeScript在编译时,会讲枚举成员引用替换为它们的实际值,而不是生成额外的枚举对象。这样可以减少生成的JavaScript代码量,提高运行时候的性能。
type type的作用是用于给类型起别名,这可以使编写代码更加的高效,增强代码可读性
1 2 3 4 type shuzi = number ;let num : shuzi = 123 ;
内联类型 将多个可能的类型合并为一个新类型,这个类型可以是多个类型中的任意一个(类型与类型之间是或的关系)
1 2 3 4 5 6 7 8 9 10 11 12 13 type shuzi = number | string ;type zifu = string | boolean ;type gender = "男" | "女" | "保密" ;let shuzi1 : shuzi = 1 ;let shuzi2 : shuzi = "1" ;let zifu1 : zifu = "1" ;let zifu2 : zifu = false ;let gender1 : gender = "保密" ;let gender2 : gender = "男" ;let gender3 : gender = "女" ;
上述代码gender类型填写具体值时会出现提示选择。
交叉类型 将多个可能的类型合并为一个新类型,这个类型必须满足所有类型的要求(类型与类型之间是且的关系)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Person = { name : string ; age : number ; }; type Address = { city : string ; country : string ; }; type demo = Person & Address ;const info : demo = { name : "John" , age : 30 , city : "New York" , country : "USA" , };
如果交叉类型的类型之间不存在交集,那么ts会自动推断为never
1 type demo = string & number
一个特殊的情况 在函数定义的时候,我们就规定了函数的返回值为void
1 2 3 4 5 6 7 8 function addNumber (a: number , b: number ): void { console .log (a + b); return undefined ; }
这个时候,按照在上文中介绍的void
的使用规则没有发生任何变化,它还是只能接受undefined
作为返回值。
但是如果我们采用在object
中的方法,先定义函数的返回值类型为void
,那么就会出现如下的反常现象。
1 2 3 4 5 6 7 8 9 10 11 let add : (a: number , b: number ) => void ;add = (a, b ) => 999 ; add = function (a, b ) { return 999 ; }; add = (a, b ) => { return 999 ; };
从上述代码可以看到,即便是规定了函数返回值为void
,但是还是能够返回除了undefined
的其他任意值。
按照官方文档的描述,这是为了兼容如下的情况
1 2 3 4 const src = [1 , 2 , 3 ];const dst = [0 ];src.forEach ((el ) => dst.push (el));
forEach的文档为
(method) Array*<number*> .forEach(callbackfn: (value*:* number , index*:* number , array*:* number []) => void , thisArg*?:* any): void
可以看到callbackfn的返回值为void,但是(el) => dst.push(el)
方法得到的返回值是dst的长度,这样会出现不兼容的情况,这算是一个妥协。
但是返回值还是符合void值无法被使用的情况。
1 2 3 4 5 6 7 8 9 10 11 let add : (a: number , b: number ) => void ;add = (a, b ) => 999 ; let result = add (10 , 20 );if (result){ } let resultPlus = result + 100 ;
复习类的知识 在ts中,类的定义和js没有明显的不同,比较明显的不同在使用constructor
的时候,需要先在类的开头申明。重写父类方法时,可以使用override
来获取重写的提示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Person { name : string ; age : number ; constructor (name: string , age: number ) { this .name = name; this .age = age; } say (): void { console .log (`I am ${this .name} , I am ${this .age} years old.` ); } } const p = new Person ("Tom" , 18 );console .log (p);p.say (); class Student extends Person { grade : string ; constructor (name: string , age: number , grade: string ) { super (name, age); this .grade = grade; } study (): void { console .log (`I am ${this .name} , I am studying.` ); } override say (): void { console .log ( `I am ${this .name} , I am ${this .age} years old, I am a student.` ); } } const stu = new Student ("Jerry" , 20 , "大三" );console .log (stu);stu.say ();
在ts中,constructor
有一个简写方法,这在后文的属性修饰符中会讲到
属性修饰符 属性修饰符是用来限定类中属性与方法的权限,写在类属性或者方法之前,例如public name: string;
修饰符
含义
具体规则
public
公开的
可以被:内部类、子类、类外部访问
protected
受保护的
可以被:类内部、子类访问
private
私有的
可以被:类内部访问
readonly
只读属性
属性无法修改
public 顾名思义,即表示属性是公开的,如果不限定属性的修饰符,那么ts默认认定属性被public
所修饰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Person { public name : string ; public age : number ; constructor (name: string , age: number ) { this .name = name; this .age = age; } public say (): void { console .log (`I am ${this .name} , I am ${this .age} years old.` ); } } class Student extends Person { public grade : string ; constructor (name: string , age: number , grade: string ) { super (name, age); this .grade = grade; } study ( ) { console .log ("I am a student. I am studying." ); } } const p = new Student ("Tom" , 18 );p.name ; p.age ; p.grade ; p.say (); p.study ();
如果属性有属性写了修饰符,那么不建议省略public
属性的简写形式 在ts中,可以使用如下的形式简写constructor
函数
1 2 3 class 类名 { constructor (修饰符 A: 类型, 修饰符 B: 类型 ) }
将上文中类的代码简写为如下形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person { constructor (public name: string , public age: number ) {} public say (): void { console .log (`I am ${this .name} , I am ${this .age} years old.` ); } } class Student extends Person { constructor ( public name: string , public age: number , public grader: string ) { super (name, age); } study ( ) { console .log ("I am a student. I am studying." ); } }
需要注意的是,子类中,如果需要新传入属性,那么在构造函数constructor
中super()
是无法省略的。
protected protected
直译为收到保护的,含义是限制外部的访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Person { constructor (protected name: string , protected age: number ) {} protected getDetails (): string { return `Name: ${this .name} , Age: ${this .age} ` ; } introduce (): void { console .log (this .getDetails ()); } } class Student extends Person { constructor (name: string , age: number , protected grade: string ) { super (name, age); } study (): void { this .introduce (); console .log (`${this .name} is studying` ); } } const p1 = new Person ("Alice" , 30 );p1.introduce (); p1.name ; p1.age ; p1.getDetails (); const s1 = new Student ("Bob" , 20 , "A" );s1.study (); s1.name ; s1.age ; s1.grade ;
上述代码中,age
和name
是收保护的属性,它能够被类内部和继承类Student
调用,但是无法被外部调用。在Student
类中调用了introduce
方法,introduce
又实现了收保护的getDetails
方法,这说明继承类能够调用父类中受到保护的protected
方法。它们都不能被外部调用。
private 意味私有的,只能在当前类中被访问,不能被子类和外部访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Person { constructor ( public name: string , public age: number , private idCard: string ) {} private getPrivateInfo (): string { return `My ID card is ${this .idCard} ` ; } getInfo (): string { return `My name is ${this .name} , I'm ${this .age} years old` ; } getFullInfo (): string { return `${this .getInfo()} , ${this .getPrivateInfo()} ` ; } } class Student extends Person { getStudentInfo (): string { this .idCard ; this .getPrivateInfo (); return this .getFullInfo (); } } const p1 = new Person ("Alice" , 20 , "123456789" );console .log (p1.getInfo ()); console .log (p1.getFullInfo ()); console .log (p1.idCard ); console .log (p1.getPrivateInfo ());
上述代码中,私有方法getPrivateInfo
可以被类中的getFullInfo
调用,但是无法被继承的Student
和外部调用。私有属性idCard
可以被类中的私有方法getPrivateInfo
调用,但是不能被继承的Student
和外部调用。
实际上,在外部调用getFullInfo
间接调用了getPrivateInfo
方法
readonly 顾名思义,即只可读的,不可被修改,写在属性修饰符之后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Person { constructor ( public name: string , public readonly age: number ) {} getInfo (): string { return `${this .name} is ${this .age} years old` ; } } const person = new Person ('John' , 30 );person.getInfo (); person.name = 'Tom' ; person.getInfo (); person.age = 31 ;
上述代码中,Person的age
属性为只读属性,因此无法被修改。
抽象类 概述:抽象类是一种无法被实例化的类 ,专门用来定义类的结构和行为,类中可以写抽象方法,也可以具体实现 ,拙象类主要用来为其派生类提供一个基础结构,要求其派生类必须实现其中的抽象方法。
简记:抽象类不能实例化,其意义是可以被继承,抽象类里可以有普通方法,也可以有抽象方法
限定一个抽象的package
对象
1 2 3 4 5 6 7 8 9 abstract class Package { constructor (public weight: number ) {} abstract calculate (): number ; printPackage ( ) { console .log (`Weight: ${this .weight} Price: ${this .calculate()} ` ); } }
在抽象对象中,规定了package的calculate
的计算方法,这个方法必须被继承的子类实现。因为printPackage
调用了calculate
这个抽象方法,但是抽象方法却没有具体的实现方式,如果成功调用会出现严重的问题,这个也可以解释抽象类是不能被实例化的。
根据快递种类的不同,定义StandardPackage
和ExpressPackage
两个类
1 2 3 4 5 6 7 8 9 class StandardPackage extends Package { constructor (weight: number , public unitPrice: number ) { super (weight); } calculate (): number { return this .weight * this .unitPrice ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ExpressPackage extends Package { constructor ( weight: number , public unitPrice: number , public additional: number ) { super (weight); } calculate (): number { if (this .weight <= 10 ) { return this .weight * this .unitPrice ; } else { return this .unitPrice * 10 + (this .weight - 10 ) * this .additional ; } } }
可以看到两种快递的计算方式不同,但是都需要计算费用这个方法。因此从本质上来说,abstract类用来规定子类必须有相同的方法,但是可以有不同的实现方式(可以解释为什么abstract
不能有具体的实现方式)
1 2 3 4 5 const p1 = new StandardPackage (13 , 8 );p1.printPackage (); const p2 = new ExpressPackage (13 , 8 , 2 );p2.printPackage ();
何时可以使用抽象类
定义通用的接口:为一组相关的类定义通用的行为(方法或属性)时
提供基础的实现:在抽象类中提供某些方法或者为其提供基础实现,这样派生类就可以继承这些实现
确保关键实现:强制派生类实现一些关键行为
共享代码和逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复
interface(接口) interface
是一种定义结构的方式,主要的作用是为类,对象、函数等规定一种契约,这样可以确保代码的一致性和类型安全,但要注意interface
只能定义格式,不能包含任何具体实现。
定义类的结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 interface PersonInterface { name : string ; age : number ; sayHello (n : number ): void ; } class Person implements PersonInterface { constructor ( public name: string , public age: number , public grade: string ) {} sayHello (n : number ): void { console .log (`Hello ${this .name} ${n} ` ); } showMyGrade (): void { console .log (`My grade is ${this .grade} ` ); } } const p = new Person ("John" , 30 , "A" );p.sayHello (10 ); p.showMyGrade ();
接口PersonInterface
定义了实现类的一些约束,实现类必须实现这些约束,在此基础上,也可以有自己的方法。
接口有两种常用的命名规范:
在接口名前添加I
,如IPerson
在接口名后添加Interface
,如PersonInterface
定义对象的结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface PersonInterface { name : string ; age : number ; run (n : number ): void ; } const person : PersonInterface = { name : "John" , age : 20 , run : (n: number ) => { console .log (`${person.name} is running ${n} miles` ); }, }; person.run (5 );
对象在继承接口的时候不需要使用implements
,且person
对象不能出现PersonInterface
规定之外的其他属性和方法。这种接口的作用和type
的作用几乎没有任何区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type PersonInterface = { name : string ; age : number ; run (n : number ): void ; } const person : PersonInterface = { name : "John" , age : 20 , run : (n: number ) => { console .log (`${person.name} is running ${n} miles` ); }, }; person.run (5 );
定义函数的结构 1 2 3 4 5 6 7 interface AddInterface { (a : number , b : number ): number ; } const add : AddInterface = (a, b ) => { return a + b; };
这个和之前的函数申明有异曲同工之妙。
1 2 3 let AddType : (a: number , b: number ) => number ;AddType = (a, b ) => a + b;
接口之间继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 interface PersonInterface { name : string ; age?: number ; } interface Student extends PersonInterface { grader : string } const student :Student = { name : 'John Doe' , grader : 'A' }
接口之间可以互相继承,继承后的接口会将父类的限制添加到自身。简而言之,实现的子类要实现所有之前所有父类的定义属性和方法。
接口的自动合并(可重复定义) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 enum Gender { man = "男" , woman = "女" , } interface PersonInterface { name : string ; age : number ; } interface PersonInterface { gender : Gender ; } const person : PersonInterface = { name : "Tom" , age : 24 , gender : Gender .man , }; console .log (person);
当接口出现命名重复的时候,接口的实现并不会相互覆盖,而是出现自动合并。 可以理解为同名接口的继承(但实际上没有这种语法,ts编译器会出现报错类型“PersonInterface”以递归方式将自身引用为基类。
)。
总结:何时使用接口
定义对象的格式:描述数据模型、API响应格式、配置对象等等,是开发中用的最多的场景
类的契约:规定一个类需要实现哪些属性和方法
自动合并:一般用于扩展第三方库的类型,这种特性在大型项目中可能会用到
一些相似概念的区别 接口与type 接口interface
和type
都可以限制对象结构,在使用方面是可以相互替代的。两者都有一些自己的特点。
interface
:更加专注于定义对象和类的结构,支持继承和合并
type
:可以定义类型别名、联合类型、交叉类型,但不支持继承和自动合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type PersonType = { name : string ; age : number ; run (n : number ): void ; }; interface PersonInterface { name : string ; age : number ; run (n : number ): void ; } const p1 : PersonInterface = { name : "John" , age : 20 , run : (n: number ) => { console .log (p1.name + " Running " + n + " miles" ); }, }; const p2 : PersonType = { name : "Tom" , age : 16 , run : (n: number ) => { console .log (p2.name + " Running " + n + " miles" ); }, }; p1.run (10 ); p2.run (20 );
观察上述代码可以看出,PersonType
和PersonInterface
的作用是一样的,都限制了对象的结构,甚至实现的结构也完全相同,两者可以相互对调。
但interface
可以进行继承和自动合并,这是type
不具备的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface PersonInterface { name : string ; age : number ; } interface PersonInterface2 extends PersonInterface { gender : string ; } const p1 : PersonInterface2 = { name : "John" , age : 20 , gender :"man" , };
上述写法可以等价于下面的写法(自动合并)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface PersonInterface { name : string ; age : number ; } interface PersonInterface { gender : string ; } const p1 : PersonInterface = { name : "John" , age : 20 , gender : "man" , };
另一方面,type
虽然不能使用继承,但可以通过交叉类型来实现上述的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type PersonInterface = { name : string ; age : number ; } type PersonType = PersonInterface & { gender : string ; } const p1 : PersonType = { name : "John" , age : 20 , gender : "man" , };
实际上,interface
比type
写法更简洁
接口与抽象类
相同点:接口和抽象类都可以定义一个类的格式
不同点:
接口:只能描述结构,不能有任何代码实现,一个类可以实现多个接口
抽象类:既可以包含抽象方法,也可以含有具体实现,一个类只能继承一个抽象类
一个类可以一次性继承多个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface FlyInterface { fly (): void ; } interface SwimInterface { swim (): void ; } class Duck implements FlyInterface , SwimInterface { fly ( ) { console .log ("Duck is flying" ); } swim ( ) { console .log ("Duck is swimming" ); } } const duck = new Duck ();duck.fly (); duck.swim ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 abstract class Duck { abstract fly (): void ; abstract swim (): void ; } class MallardDuck extends Duck { fly ( ) { console .log ("MallardDuck is flying" ); } swim ( ) { console .log ("MallardDuck is swimming" ); } }
接口只动口不动手,抽象类是可以自己动手的。
泛型 泛型允许我们在定义函数、类或接口时,使用类型参数来表示未指定类型,这些参数在具体使用时,才被置顶具体的类型。这能让同一代码使用多数据类型,同时保持类型的安全性。
泛型函数 当我们暂时无法确定要传入的参数的类型时,可以使用泛型告知编译器参数可以是任意的类型。
1 2 3 4 5 6 7 8 function printData<T>(data1 : T): void { console .log (data1); } printData<string >("Hello World" ); printData<number >(123 ); printData<boolean >(true );
当时当使用者调用的时候,需要提前告知其传入的参数类型。
实际上<T>
中的T只是一个标识符,可以替换为任意的字符
多个泛型 泛型可以不只有一个
1 2 3 4 5 6 7 function printData<T, U>(data1 : T, data2 : U): void { console .log (data1, data2); } printData<string , number >("Hello World" , 222 ); printData<number , boolean >(123 , false );
返回值也可以设置为泛型
1 2 3 4 5 function printData<T, U>(data1 : T, data2 : U): T | U { return Math .random () % 2 === 0 ? data1 : data2; } console .log (printData<string , number >('Hello' , 123 ));
泛型接口 在接口中也可以使用泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 interface PersonInterface <T> { name : string ; age : number ; moreInfo : T; } type PE = { height : number ; weight : number ; }; const person : PersonInterface <PE > = { name : "John" , age : 30 , moreInfo : { height : 180 , weight : 80 , }, };
泛型约束 可以使用继承的方式限定泛型的取值范围
1 2 3 4 5 6 7 8 9 10 interface PersonInterface { name : string ; age : number ; } function logPerson<T extends PersonInterface >(info : T): void { console .log (`My name is ${info.name} and I am ${info.age} years old.` ); } logPerson ({ name : "John" , age : 30 });
泛型类 泛型可以在类中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Student <T> { constructor (public name: string , public age: number , public moreInfo: T ) {} speak ( ) { console .log (`My name is ${this .name} , I am ${this .age} years old` ); console .log (this .moreInfo ); } } type jobInfo = { job : string ; salary : number ; }; const student = new Student <jobInfo>("Tom" , 20 , { job : "developer" , salary : 1000 , }); student.speak ()
类型申明文件 在npm官网,还有很多的库是使用js编写的,不能直接在ts中导入。
新建一个demo.js
文件,导出相应的模块
1 2 3 4 5 6 7 8 export function add (a, b ) { return a + b; } export function abstract (a, b ) { return a - b; }
这时候如果直接在index.ts
中导入,就会出现无法找到模块“./demo.js”的声明文件。“d:/Web/ts/demo.js”隐式拥有 "any" 类型。
的报错,这是因为js不支持编写类型提示。
1 2 3 4 5 6 import { add, abstract } from "./demo.js" ;console .log (add (1 , 2 ));console .log (abstract (1 , 2 ));
此时需要在文件中新建一个同名的demo.d.ts
文件,加入如下代码
1 2 3 4 5 declare function add (a: number , b: number ): number ;declare function abstract (a: number , b: number ): number ;export { add, abstract };
declare
意味申明,这样可以告知ts编译器同名的demo.js
中数据的具体类型。编写后需要重启vscode,之后就可以在index.ts
中悬停得到相应的代码提示。
注:
在项目,这些文件通常放在@types
文件夹中
对于一些老的js库,可以在npm按照@types/库名
的方式直接搜到相应的.d.ts
文件。如@types/jquery