简介
参考文档
- https://ts.xcatliu.com/ 阮一峰typescript入门教程
- TypeScript: JavaScript With Syntax For Types. (typescriptlang.org) typescript官网
什么是TypeScript
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上。
它的第一个版本发布于 2012 年 10 月,经历了多次更新后,现在已成为前端社区中不可忽视的力量,不仅在 Microsoft 内部得到广泛运用,而且 Google 开发的 Angular 从 2.0 开始就使用了 TypeScript 作为开发语言,Vue 3.0 也使用 TypeScript 进行了重构。
总结
- TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
- TypeScript 是一门静态类型、弱类型的语言。
- TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。
- TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。
- TypeScript 拥有很多编译选项,类型检查的严格程度由你决定。
- TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript。
- TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。
- TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明。
- TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)。
安装 TypeScript
npm install -g typescript
以上命令会在全局环境下安装 tsc
命令,安装完成之后,我们就可以在任何地方执行 tsc
命令了。
编译一个 TypeScript 文件很简单:
tsc hello.ts
我们约定使用 TypeScript 编写的文件以 .ts
为后缀,用 TypeScript 编写 React 时,以 .tsx
为后缀。
Hello TypeScript
首先编写 hello.ts
function sayHello(person: string) {
return 'Hello' + person
}
let user = 'Tom'
console.log(sayHello(user))
然后执行编译命令
tsc hello.ts
这时候会生成一个 hello.js
,内容如下
function sayHello(person) {
return 'Hello' + person;
}
var user = 'Tom';
console.log(sayHello(user));
在 TypeScript 中,我们使用 :
指定变量的类型,:
的前后有没有空格都可以。
上述例子中,我们用 :
指定 person
参数类型为 string
。但是编译为 js 之后,并没有什么检查的代码被插入进来。
这是因为 TypeScript 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错。而在运行时,与普通的 JavaScript 文件一样,不会对类型进行检查。
现在我们来修改一下 hello.ts
function sayHello(person: string) {
return 'Hello' + person
}
let user = ['Tom','Jack']
console.log(sayHello(user))
然后重新执行编译命令,发现报如下错误
但是 hello.js
文件仍然正常生成
function sayHello(person) {
return 'Hello' + person;
}
var user = ['Tom', 'Jack'];
console.log(sayHello(user));
这是因为 ts 只会在编译时报错,但是在运行时不会提示错误
基础
原始数据类型
JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
原始数据类型包括:布尔值、数值、字符串、null
、undefined
以及 ES6 中的新类型 Symbol
和 ES10 中的新类型 BigInt
。
布尔值(boolean)
布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean
定义布尔值类型
let boolean: boolean = false
数值(number)
使用 number
定义数值类型
let num1: number = 123
let num2: number = 12.3
let num3: number = NaN
字符串(string)
使用 string
定义字符串类型
let str1: string = 'hello'
let str2: string = 'typescript'
let str3: string = `${str1} ${str2}`
空值(void)
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void
表示没有任何返回值的函数
function returnVoid(): void {
console.log('这是一个返回空值的函数');
}
let unde: void = undefined
null 和 undefined
在 TypeScript 中,可以使用 null
和 undefined
来定义这两个原始数据类型
let n: null = null
let u: undefined = undefined
任意类型
声明方式
任意类型使用 any 关键字来表示。原始数据类型声明过类型后不能再赋值其他类型的数据,而任意值的类型可以赋值成多种数据类型
let anystr: any = 'hello'
anystr = 123
anystr = false
任意值的属性和方法
任意值属性可以访问任意方法和属性
// 下面写法不会出错
let anyThing: any = 'hello';
console.log(anyThing.length);
console.log(anyThing.toFixed(2));
// 下面的写法会提示错误
// 属性“toFixed”在类型“string”上不存在。你是否指的是“fixed”?
let anyTest: string = "hello"
anyTest.toFixed(2)
可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值
未声明类型变量
如果一个变量没有声明类型时,则默认也是任意值类型
let something;
something = 'seven';
something = 7;
something.setName('Tom');
等同于
let something: any;
something = 'seven';
something = 7;
something.setName('Tom');
类型推论
下面的代码虽然没有声明变量的类型,但是会报错
let myFavoriteNumber = 'save'
myFavoriteNumber = 7 // 不能将类型“number”分配给类型“string”
这个代码等同于
let myFavoriteNumber: string = 'save'
myFavoriteNumber = 7 // 不能将类型“number”分配给类型“string”
如果给变量声明了初始值,则ts会根据设置初始值类型推测出这个变量的数据类型,这就是类型推论
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any
类型而完全不被类型检查:
let notSetInitVal
notSetInitVal = 7
notSetInitVal = 'save'
联合类型
表示取值可以是多种类型中的一种
简单的举例
let str4: number | string = 56
str4 = 'string'
// str4 = false // 不能将类型“boolean”分配给类型“string | number”
多个类型使用 |
分隔,赋值时可以是声明了的类型,不能声明额外的数据类型
访问联合类型的属性或方法
当联合类型属性的值不确定时,只能访问联合类型中共用的属性或者方法,下面代码访问 num.toFixed(2)
出错是因为 string 类型没有这个方法
// 访问联合类型的属性或方法
function str5(num: string | number): any {
// num.toFixed(2) 类型“string | number”上不存在属性“toFixed”。类型“string”上不存在属性“toFixed”。
return num.toString()
}
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型
let str6: string | number
str6 = 'hello'
console.log(str6.length);
str6 = 6
console.log(str6.toFixed());
上面的代码首先类型推断为字符串类型,所以可以使用 length 属性,然后又赋值为数字类型,所以可以使用 toFixed 方法
对象的类型-接口
什么是接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
简单的例子
interface Person {
name: string,
age: number
}
let str7: Person = {
name: "李四",
age: 18,
}
上面的例子中我们声明了一个接口 Person,并规定里面有两个属性,一个是 string 类型的 name,一个是 number 类型的 age。然后什么一个 str7 来实现这个接口,实现接口的对象的形状必须和接口一致,多一个属性或者少一个属性都不行
多一个时:
少一个时:
可见,赋值的时候,变量的形状必须和接口的形状保持一致
可选属性
有时我们希望不要完全匹配一个形状,那么可以用可选属性。
interface Status {
name: string
age?: number
}
let str8: Status = {
name: "张三"
}
在属性名称后面添加一个 ?
表示这是一个可选属性,实现这个接口时可以不声明这个属性,但是此时添加额外属性还是不行的
任意属性
有时候我们希望一个接口允许有任意的属性,可以使用如下方式
interface Order {
name: string,
age?: number,
// 如果定义了任意属性,则其他属性的类型都必须是任意属性类型的子集
[propName: string]: any
}
let str9: Order = {
name: 'Tom',
age: 6,
gender: 'male'
}
注意:如果定义了任意属性,则其他属性的类型都必须是任意属性类型的子集。所以上面的接口中定义的任意属性的类型为 any
只读属性
如果希望一个属性只能在初始化时赋值,然后不能更改。可以使用 readonly 关键字定义只读属性
interface ReadOnly {
readonly id: number
name: string
age?: number
[propName: string]: any
}
let str10: ReadOnly = {
id: 10,
name: "王五",
age: 8,
height: 185
}
// 下面的代码会提示: 无法为“id”赋值,因为它是只读属性
// str10.id = 11
数组的类型
在 typescript 中,数组类型有多中定义方法,比较灵活
类型+方括号表示法
声明了数组类型后,数组中不能有其他类型的数据,这是最常用的表达方式
// number 类型的数组
let numarr: number[] = [1, 2, 3]
// string 类型的数组
let strarr: string[] = ['a', 'b', 'c']
数组的一些方法参数也会收到类型影响
// 类型“number”的参数不能赋给类型“string”的参数
// strarr.push(9)
数组泛型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
泛型具体讲解在后面章节
用接口标识数组
interface StrArr {
[id: number]: number
}
let arr1: StrArr = [1, 2, 3]
一般不这样做
类数组
类数组不是数组类型,比如 argument
function sum() {
// 类型“IArguments”缺少类型“string[]”的以下属性: pop, push, concat, join 及其他 26 项
let arg: string[] = arguments
}
arguments 不是一个普通的数组,所以不能用普通的数组来表示,必须使用类数组
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
事实上常用的类数组都有自己的接口定义,如 IArguments
, NodeList
, HTMLCollection
等:
function sum2() {
let args: IArguments = arguments;
}
其中 IArguments
是 TypeScript 中定义好了的类型,它实际上就是:
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
关于内置对象,查看后面的章节
any 在数组中的应用
any 表示数组中可以是任意类型的值
let objarr:any[] = [
1,
'hello',
{
name:"李四"
}
]
函数类型
函数的声明
普通方式声明函数
// 在typescript中当函数有输入和输出时,都要吧参数类型声明好
function sum2(a: number, b: number): number {
return a + b
}
函数表达式来声明函数
// ts版本的函数表达式
const sum3 = (a: number, b: number): number => {
return a + b
}
函数的参数一旦定义完成后,少传或者多传参数都是不允许的
// 输入多余的参数或者少于参数都是不允许的
// sum2(1, 2, 3) //=> 应有 2 个参数,但获得 3 个
用接口定义函数形状
// 用接口定义函数形状
interface SearchFun {
// 冒号左边声明这个函数有哪些参数
// 右边定义这个函数的返回值类型
(source:string,subSource:string) : boolean
}
// 声明一个值来实现接口
let mySearch:SearchFun
// 按照接口形状定义函数
mySearch = function(source:string,subSource:string){
// 返回值类型必须和接口定义的返回值一样
return source.search(subSource) !== -1
}
可选参数
上面提到,参数声明好之后少传或者多传都是不允许的,但是实际情况中我们有的参数时有时无,这种时候我们可以使用可选参数
可选参数使用 ?
来表示
const sum4 = (a: number, b: number, c?: number): number => {
if (c) {
return a + b + c
} else {
return a + b
}
}
sum4(1, 2, 3)
sum4(1, 2)
这里需要注意的是,可选参数的位置必须放在最后面,否则报错
参数默认值
// 参数默认值
const sum5 = (a: number = 1, b: number): number => {
return a + b
}
sum5(2, 5)
sum5(undefined, 5)
剩余参数
// 剩余参数
const sum6 = (a: string, ...args: string[]): string => {
let str = a
args.forEach(item => {
a += item
})
return str
}
方法重载
// 首先定义这个方法有那几种传参和返回的可能
function reverse(x: string): string
function reverse(x: number): number
// 最后必须要实现这个方法
function reverse(x: number | string): string | number {
if (typeof x === 'string') {
return x.split("").reverse().join()
} else if (typeof x === 'number') {
return Number(x.toString().split("").reverse().join())
} else {
return ""
}
}
上例中,我们重复定义了多次函数 reverse
,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
类型断言
可以手动指定一个值的类型
语法:
值 as 类型
将联合类型断言为其中一个类型
前面提到,在ts中不确定一个联合类型的变量是哪一个的时候,只能访问此联合类型中所有类型共有的属性或方法
// 类型断言
interface Cart {
name: string
run(): void
}
interface Fish {
name: string
swit(): void
}
function getName(animal: Cart | Fish): string {
return animal.name
}
然而有时候,我们必须在不确定是那个类型的时候使用其中一个类型的属性,例如下面的代码
// 类型断言
interface Cart {
name: string
run(): void
}
interface Fish {
name: string
swit(): void
}
function getName(animal: Cart | Fish): string {
// 使用类型断言,将animal断言为Fish
if (typeof animal.swit === 'function') {
return 'fish'
} else {
return 'cart'
}
}
// 报错:类型“Cart | Fish”上不存在属性“swit”。类型“Cart”上不存在属性“swit”
这个时候就要使用断言
// 类型断言
interface Cart {
name: string
run(): void
}
interface Fish {
name: string
swit(): void
}
function getName(animal: Cart | Fish): string {
// 使用类型断言,将animal断言为Fish
if (typeof (animal as Fish).swit === 'function') {
return 'fish'
} else {
return 'cart'
}
}
将一个父类断言为一个具体的子类
// 将一个父类断言为一个具体的子类
interface ApiError extends Error {
code: number
}
interface HttpError extends Error {
statusCode: number
}
function isOk(err: Error) {
if ((err as ApiError).code === 200) {
return true
} else {
return false
}
}
函数isOK接收的是Error类型的参数,但是Error上没有code属性,所以将err参数断言为ApiError,就可以使用code属性进行条件判断
将任何一个类型断言为any
首先看下面的代码
let num: number = 1
console.log(num.length);
//=>err:类型“number”上不存在属性“length”
当我们访问了一个类型上不存在属性或者方法时,ts会明确的给我们提示出错误原因,这一点是很有用的。
但是有时候我们必须要访问一个不存在的属性,例如:
window.userId = 1001
//=> err: 类型“Window & typeof globalThis”上不存在属性“userId”
我们想在window上添加一个userId属性,这个时候ts会提示错误,我们可以使用 as any,来避免错误
(window as any).userId = 1001
注意的是这种语法会掩盖原有的错误提示,但是在开发中有时候这种写法反而可以提高开发效率
将any断言成一个具体的类型
// 将any类型断言成一个具体的类型
function getCatchData(key: string): any {
return {
userName: key,
run: () => {
console.log(key);
}
}
}
interface Cart {
userName: string
run(): void
}
let Tom = getCatchData('李四') as Cart
// 经过断言后,就会有代码提示
console.log(Tom.userName);
类型断言的限制
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A
能够被断言为B
,只需要A
兼容B
或B
兼容A
即可
声明文件
新语法索引
declare var
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块export as namespace
UMD 库声明全局变量declare global
扩展全局变量declare module
扩展模块///
三斜线指令
什么是声明语句
例如我们在使用jQuery时,我们想要通过一个id获得元素,我们可以这样做
$('#id')
// or
jQuery('#id')
但是在ts中,会提示:找不到名称“jQuery”,这时候需要使用 declare var 来声明
declare var jQuery: (selector: string) => any;
jQuery('#foo');
上例中,declare var
并没有真的定义一个变量,只是定义了全局变量 jQuery
的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:
jQuery('#foo');
声明文件
通常我们会把声明专门放在一个文件中,这个文件就称为声明文件
例如新建 jQuery.d.ts
,然后里面添加代码
declare var jQuery: (selector: string) => any;
声明文件必须以 d.ts
结尾,typescript 会扫描所有以 .ts 结尾的文件,所以声明文件也会扫描到。然后再使用 jQuery 就不会出错了
当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped。
我们可以直接下载下来使用,但是更推荐的是使用 @types
统一管理第三方库的声明文件。
@types
的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
npm install @types/jquery --save-dev
可以在这个页面搜索你需要的声明文件
书写声明文件
点击这里查看
内置对象
JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。
内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。
ECMAScript 的内置对象
ECMAScript 标准提供的内置对象有:
Boolean`、、、 等。`Error``Date``RegExp
我们可以在 TypeScript 中将变量定义为这些类型:
let err: Error = new Error("this is a error")
let boolean: Boolean = false
let date: Date = new Date()
let rege: RegExp = /[1-9]/
DOM 和 BOM 的内置对象
DOM 和 BOM 提供的内置对象有:
Document`、、、 等。`HTMLElement``Event``NodeList
TypeScript 中会经常用到这些类型:
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
它们的定义文件同样在 TypeScript 核心库的定义文件中。
用 TypeScript 写 Node.js
Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:
npm install @types/node --save-dev
进阶
类型别名
使用关键词 type
来声明一个类型别名
// 定义str表示string类型
type str = string
// 定义numfun变量表示一个接收number类型并返回number类型的函数
type numfun = (a: number) => number
// 使用strOrNumFun变量表示一个联合类型
type strOrNumFun = str | numfun
function a(x: strOrNumFun) {
if (typeof x === 'string') {
return x
} else {
x(5)
}
}
字符串字面量类型
// 声明一个类型depts,值只能是下面定义的三个部门
type depts = '销售部' | '开发部' | '商务部'
// 定义一个接口,里面有name,和dept
interface userInfo {
name: string
dept: string
}
// 定义一个函数,传入一个userinfo和一个部门名称,然后设置用户信息中的部门名称
function setUserDept(userinfo: userInfo, dept: depts) {
userinfo.dept = dept
}
// 定义 Jack 这个人的信息
let Jack: userInfo = {
name: "Jack",
dept: ""
}
// 调用这个接口时,部门名称只能是上面定义的三个部门中的一个
setUserDept(Jack, "商务部")
//=> err: 类型“"法务部"”的参数不能赋给类型“depts”的参数
// setUserDept(Jack, "法务部")
元祖
数组是吧相同类型的数据放在一个中括号中,而元祖是吧不同类型的元素放在一个中括号中
初始化赋值
// 元祖
type y = [string, number]
// 初始化赋值
let yuan1: y = ['hello', 123]
// 初始化后赋值,在变量后面添加 ! 表示这个变量一定有值,从而可以实现无需初始化值直接使用
let yuan2!: y
yuan2[0] = 'hello'
yuan2[1] = 123
直接对元祖赋值的时候。需要提供元祖中的所有类型值
//=> err:不能将类型“[string]”分配给类型“y”。源具有 1 个元素,但目标需要 2 个
// yuan2 = ['hello']
越界的元素
使用 push 方法可以往元祖中追加元素,当超出初始化的长度时,则超出的元素类型将是元祖中每个类型的联合类型
//=> err:类型“boolean”的参数不能赋给类型“string | number”的参数
yuan2.push(true)
TS中的!和?用法
- 属性或参数中使用 ?:表示该属性或参数为可选项
- 属性或参数中使用 !:表示强制解析(告诉typescript编译器,这里一定有值),常用于vue-decorator中的@Prop
- 变量后使用 !:表示类型推断排除null、undefined
枚举
枚举类型用于取值被限制在某一范围内的场景,比如一周只能有7天,颜色只能是红黄蓝
简单的例子
使用 enum
来定义枚举类型
// 定义枚举类型
enum weekDays { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
// 枚举值会从0开始递增
let sun = weekDays.Sun
console.log(sun === 0); // true
console.log(weekDays.Mon === 1); // true
console.log(weekDays.Tue === 2); // true
// 枚举值也会反向映射枚举名
console.log(weekDays[0] === 'Sun'); // true
console.log(weekDays[1] === 'Sun'); // false
声明的代码编译成JS后是这样的
var weekDays;
(function (weekDays) {
weekDays[weekDays["Sun"] = 0] = "Sun";
weekDays[weekDays["Mon"] = 1] = "Mon";
weekDays[weekDays["Tue"] = 2] = "Tue";
weekDays[weekDays["Wed"] = 3] = "Wed";
weekDays[weekDays["Thu"] = 4] = "Thu";
weekDays[weekDays["Fri"] = 5] = "Fri";
weekDays[weekDays["Sat"] = 6] = "Sat";
})(weekDays || (weekDays = {}));
;
手动赋值
// 手动赋值枚举类型
enum weekDays2 {
Sun = 5, Mon = 3, Tue, Wed, Thu, Fri, Sat
}
console.log(weekDays2.Sun); //=> 5
// 未手动赋值的枚举会从上一个赋值的枚举值继续往后递增
console.log(weekDays2[5]); //=> Wed
// 赋值string类型
enum colorEnum { RED = "RED", YELLOR = "YELLOR", BLUE = "BLUE" }
console.log(colorEnum.RED); //=> RED
手动赋值也可以赋值小数或者负数
enum weekDays3 { Sun = -2, Mon = 1.5, Tue, Wed, Thu, Fri, Sat }
console.log(weekDays3.Sun); //=> -2
console.log(weekDays3.Tue); //=> 2.5
console.log(weekDays3.Wed); //=> 3.5
常数项和计算所得项
我们上面定义的枚举值都是常数项,而经过计算才能得到值的项目被称为计算所得项。例如
enum weekDays4 { Sun = -2, Mon = 1.5, Tue, Wed, Thu, Fri, Sat = 'sat'.length }
console.log(weekDays4.Sat); //=> 3
计算所得项后面也必须是计算所得项或者没有项
enum weekDays4 { Sun = -2, Mon = 1.5, Tue, Wed, Thu, Fri, Sat = 'sat'.length, Test = 'Test'.length }
console.log(weekDays4.Sat); //=> 3
console.log(weekDays4.Test); //=> 4
如何只定义常数枚举,可以使用关键字 const
// 定义常数枚举
const enum Directions {
Top,
Bottom,
Left,
Right,
}
JS中类的用法
类的概念
虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过
new
生成 - 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如
Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat
方法,程序会自动判断出来应该如何执行eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如
public
表示公有属性或方法 - 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
es6中类的用法
属性和方法
class Animal {
public name;
constructor(name) {
this.name = name
}
sayHai() {
console.log('hello' + this.name);
}
}
let animal = new Animal("李四")
animal.sayHai() //=> hello李四
类的继承
使用关键字 extend
// 类的继承
class Cat extends Animal {
constructor(name) {
super(name)
console.log(this.name);
}
sayHai() {
console.log('Cat sayHai');
// 调用父类的 sayHai 方法
super.sayHai()
}
}
let cat = new Cat('张三')
cat.sayHai()
存取器
在类中通过 get 和 set 方法来对某个属性进行赋值和读取操作
class Animal {
constructor(name) {
this.name = name
}
// 使用存取器来读写数据时,无需在类中初始化声明变量
get name() {
return 'Jack'
}
set name(val) {
console.log("setter" + val);
}
sayHai() {
console.log('hello' + this.name);
}
}
let animal = new Animal("李四")
// 读取name属性,实际调用的是 get name() 方法
console.log(animal.name); //=> Jack
由于类的概念是在 es6 之后才提出的,但是默认编译 ts 为 js 时会编译成 es5 的代码,所以在编译时可以指定编译后的js版本
指定 -t es6
即可将ts变为es6的js代码
tsc .\jsClass.ts -t es6
静态方法
方法前面声明关键字 static
表示这个方法为一个静态方法,不需要通过实例化对象来调用,直接通过 类名.方法名()
调用
class Animal {
constructor(name) {
this.name = name
}
static isAnimal(a){
return a instanceof Animal
}
}
let animal = new Animal("李四")
console.log(Animal.isAnimal(animal)); //=> true
es7中类的用法
实例属性
es6中类里面的属性只能通过构造函数里面的 this.xxx = xxx
来定义,在es7中可以直接在类里面定义属性
class Person{
perName = "person"
constructor(){}
}
let per = new Person()
console.log(per.perName); //=> person
静态属性
顾名思义,可以直接通过类来访问某个属性
class Person{
perName = "person"
static staticName = 'staticname'
constructor(){}
}
console.log(Person.staticName); //=> staticname
TS中类的用法
public private 和 protected
- public 表示该属性或者方法是公共的,在类的外部也是可以被访问的,默认类中所有的方法和属性都是 public
- private 表示该属性或者方法是私有的,只允许在类内部使用
- protected 修饰的属性或方法是受保护的,它和
private
类似,区别是它在子类中也是允许被访问的
通过代码来区别三者不同
class PublicClass {
public name
constructor(name) {
this.name = name
}
}
let pub = new PublicClass("Jack")
console.log(pub.name); //=> Jack
pub.name = 'Tome'
console.log(pub.name); //=> Tome
上面的代码将name属性设置成了public,所以可以在类的外部访问和赋值
class PrivateClass {
private name
constructor(name) {
this.name = name
}
}
let pri = new PrivateClass("Jack")
//=> err: 属性“name”为私有属性,只能在类“PrivateClass”中访问
console.log(pri.name);
上面的代码将name设置成了private,然后实例化一个PrivateClass类对象,通过对象来访问name时,会出现错误提示,提示:属性“name”为私有属性,只能在类“PrivateClass”中访问
class ProtectedClass {
protected name
constructor(name) {
this.name = name
}
}
class ProChildrenClass extends ProtectedClass {
constructor(name) {
super(name)
console.log(this.name); //=> Jack
}
}
let proc = new ProChildrenClass("Jack")
设置了 protected 修饰的属性,效果和 private 类似,但是 protected 修饰的属性或方法允许在子类中使用
readOnly
readOnly表示只读属性,无法修改这个属性的值
class ReadOnlyClass {
readonly name
constructor(name) {
this.name = name
}
}
let read = new ReadOnlyClass("Jack")
console.log('read.name', read.name)
//=> err: 无法为“name”赋值,因为它是只读属性
// read.name = 'Tome'
注意:如果 readonly
和其他访问修饰符同时存在的话,需要写在其后面。
class Animal {
// public readonly name;
public constructor(public readonly name) {
// this.name = name;
}
}
抽象类
abstract 用于定义抽象类的抽象方法,抽象类不允许被实例化,抽象方法必须被子类实现
abstract class AbstractClass{
name
constructor(name){
this.name = name
}
abstract sayHai()
}
//=> err: 无法创建抽象类的实例
let abs = new AbstractClass('Jack')
当实现一个抽象类时会出现错误提示
除此之外,抽象方法必须被子类实现,代码如下
当没有实现抽象方法时会出现下面的错误提示
abstract class AbstractClass{
name
constructor(name){
this.name = name
}
abstract sayHai()
}
//err=> 非抽象类“AbsChildren”不会实现继承自“AbstractClass”类的抽象成员“sayHai”
class AbsChildren extends AbstractClass{
constructor(name){
super(name)
}
}
必须实现父类的抽象方法
abstract class AbstractClass {
name
constructor(name) {
this.name = name
}
abstract sayHai()
}
class AbsChildren extends AbstractClass {
constructor(name) {
super(name)
}
sayHai() {
console.log(this.name);
}
}
let absc = new AbsChildren('Jack')
console.log('absc.name',absc.name) //=> Jack
类的类型
给类中的属性和方法添加类型限制很简单
class ClassType {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
sayHai(): string {
return `我叫${this.name},今年${this.age}岁`
}
}
let ct = new ClassType("张三",18)
console.log('ct.sayHai()',ct.sayHai()) //=> 我叫张三,今年18岁
类与接口
类实现接口
一般来讲,一个类可以继承另外一个类,多个类之间会存在相同的东西,我们可以吧这些相同的功能抽取成一个接口,让类去实现它。
举个例子:门的一个父类,防盗门是门的一个子类,现在我要给防盗门添加一个报警器,我可以实现一个报警器的接口,让防盗门去实现它,现在我又有一个车,车上面也要有报警器功能,我直接让车去实现报警器接口即可
代码实现
// 报警器接口
interface Alarm {
// 报警方法,方法的具体功能由实现类去实现
alert()
}
// 这是一个门
class Door { }
// 这是一个门的子类,防盗门
class SecurityDoor extends Door implements Alarm {
alert() {
console.log('防盗门触发警报');
}
}
// 这是一个车,实现报警器接口
class Car implements Alarm{
alert() {
console.log('车触发警报')
}
}
let sec = new SecurityDoor()
sec.alert() //=> 防盗门触发警报
let car = new Car()
car.alert() //=> 车触发警报
一个类可以实现多个接口
例如:现在除了实现报警器功能,再实现一个车灯的功能
// 报警器接口
interface Alarm {
// 报警方法,方法的具体功能由实现类去实现
alert()
}
// 车灯的接口
interface Light {
lightOn()
lightOff()
}
// 这是一个车,实现报警器接口
class Car implements Alarm, Light {
alert() {
console.log('车触发警报')
}
lightOn() {
console.log('车灯打开')
}
lightOff() {
console.log('车灯关闭')
}
}
接口继承接口
车灯接口继承报警器接口后,类去实现车灯接口时,也还是要实现三个方法
// 报警器接口
interface Alarm {
// 报警方法,方法的具体功能由实现类去实现
alert()
}
// 车灯的接口
interface Light extends Alarm{
lightOn()
lightOff()
}
// 这是一个车,实现车灯接口,但是要实现三个方法
class Car implements Light {
alert() {
console.log('车触发警报')
}
lightOn() {
console.log('车灯打开')
}
lightOff() {
console.log('车灯关闭')
}
}
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
简单的例子
function createArray(total: number, value: any): Array<any> {
let valarr: any = []
for (let i = 0; i < total; i++) {
valarr[i] = value
}
return valarr
}
createArray(3, 'x') //=> [ 'x', 'x', 'x' ]
这段代码中,valarr 数组中的每一项都是任意类型,现在我们想让数组中的元素类型是我们输入的数据类型,这时候泛型就派上用场了
// 添加泛型约束
function createArray2<T>(total: number, value: T): Array<T> {
let valarr: T[] = []
for (let i = 0; i < total; i++) {
valarr[i] = value
}
return valarr
}
// 如果泛型约束的是number,则参数的类型就必须和泛型定义的一致
//err=> 类型“string”的参数不能赋给类型“number”的参数
createArray2<number>(3,'x')
也可以不手动指定泛型,让类型推断自动推断出来数据类型
多个类型参数
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
swap([1,'hello'])
上面的代码会自动推断为:function swap<number, string>(tuple: [number, string]): [string, number]
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道属性的具体类型,所以读取一些属性的时候会报错
function getLength<T>(val:T){
//err=> 类型“T”上不存在属性“length”
console.log(val.length);
}
可以通过让泛型继承某个接口
interface witchLenth {
length: number
}
function getLength<T extends witchLenth>(val: T) {
//err=> 类型“T”上不存在属性“length”
console.log(val.length);
}
// 这个时候调用方法时必须出入有length属性的参数
getLength('hello')
//err=> 类型“number”的参数不能赋给类型“witchLenth”的参数
getLength(123)
多个类型互相约束
// 多个类型之间互相约束
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
// 让soutce拥有和target参数一样的属性,如果不判定source为T类型,则会提示source上面没有[id]属性
target[id] = (source as T)[id]
}
return target
}
let t = { a: 1, b: 2, c: 3, d: 4 }
let s = { b: 10, d: 20 }
copyFields(t, s)
泛型接口
使用泛型接口来约束函数形状
// 我们可以使用泛型接口来定义函数形状
interface CreateFun {
<T>(total: number, subString: T): Array<T>
}
let createFuns: CreateFun
createFuns = function <T>(total: number, val: T): Array<T> {
let arr: T[] = []
for (let i = 0; i < total; i++) {
arr[i] = val
}
return arr
}
可以吧泛型提前到接口上
interface CreateArrFun3<T> {
(total: number, value: T): Array<T>
}
let createFun3: CreateArrFun3<string>
createFun3 = function <T>(total, value) {
let arr: T[] = []
for (let i = 0; i < total; i++) {
arr[i] = value
}
return arr
}
泛型类
class GenerClass<T>{
name: T
constructor(name) {
this.name = name
}
sayHai() {
console.log(this.name)
}
}
let gen = new GenerClass<string>('Jack')
//err=>不能将类型“number”分配给类型“string”
gen.name = 456
泛型参数的默认类型
// 泛型的默认类型
function createArr5<T = string>(total: number, value: T): Array<T> {
let arr: T[] = []
for (let i = 0; i < total; i++) {
arr[i] = value
}
return arr
}
// 不声明泛型类型时,默认是string
createArr5(3, "x")
声明合并
如果声明了同名的函数、接口、类,那么他们会合并成一个类型
函数的合并
我们可以使用重载,定义多个函数类型
function reverse(val: number): number
function reverse(val: string): string
function reverse(val: number | string): number | string | undefined {
if (typeof val === 'number') {
return val.toString().split("").reverse().join("")
} else if (typeof val === 'string') {
return val.split("").reverse().join("")
}
}
console.log(reverse(123456)); //=> 654321
console.log(reverse('helloword')); //=> drowolleh
接口合并
interface Alarm {
time: number
}
interface Alarm {
price: number
}
let car: Alarm = {
time: 0,
price: 0
}
这里接口合并时,当属性出现重复时,必须保证重名的属性类型一致,否则会报错
interface Alarm {
time: number
}
interface Alarm {
//err=> 后续属性声明必须属于同一类型。属性“time”的类型必须为“number”,但此处却为类型“string”
time: string
price: number
}
工程
在 TypeScript 中使用 ESLint
安装Eslint
将下面的依赖安装在当前项目中
npm install --save-dev eslint
由于 ESLint 默认使用 Espree 进行语法解析,无法识别 TypeScript 的一些语法,故我们需要安装 @typescript-eslint/parser
,替代掉默认的解析器,别忘了同时安装 typescript
:
npm install --save-dev typescript @typescript-eslint/parser
接下来需要安装对应的插件 @typescript-eslint/eslint-plugin 它作为 eslint 默认规则的补充,提供了一些额外的适用于 ts 语法的规则。
npm install --save-dev @typescript-eslint/eslint-plugin
创建配置文件
ESLint 需要一个配置文件来决定对哪些规则进行检查,配置文件的名称一般是 .eslintrc.js
或 .eslintrc.json
。
当运行 ESLint 的时候检查一个文件的时候,它会首先尝试读取该文件的目录下的配置文件,然后再一级一级往上查找,将所找到的配置合并起来,作为当前被检查文件的配置。
我们在项目的根目录下创建一个 .eslintrc.js
,内容如下:
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
// 禁止使用 var
'no-var': "error",
// 优先使用 interface 而不是 type
'@typescript-eslint/consistent-type-definitions': [
"error",
"interface"
]
}
}
检查一个ts文件
创建完配置文件后,来创建一个ts文件测试eslint是否能检查它,新建一个 index.ts 并输入如下内容
var myName = 'Tom';
type Foo = {};
然后再终端运行 ./node_modules/.bin/eslint index.ts
,会出现如下错误
需要注意的是,我们使用的是 ./node_modules/.bin/eslint
,而不是全局的 eslint
脚本,这是因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中。
可是每次执行这么长一段脚本颇有不便,我们可以通过在 package.json
中添加一个 script
来创建一个 npm script 来简化这个步骤:
{
"scripts": {
"eslint": "eslint index.ts"
}
}
这时只需执行 npm run eslint
即可。
检查整个项目的ts文件
一般我们都会吧代码放在 src
目录下,我们希望检查 src 目录下的所有 ts 文件,所以需要将 package.json
中的 eslint
脚本改为对一个目录进行检查。由于 eslint
默认不会检查 .ts
后缀的文件,所以需要加上参数 --ext .ts
:
{
"scripts": {
"eslint": "eslint src --ext .ts"
}
}
此时执行 npm run eslint
即会检查 src
目录下的所有 .ts
后缀的文件。
在 VSCode 中集成 ESLint 检查
在编辑器中集成 ESLint 检查,可以在开发过程中就发现错误,甚至可以在保存时自动修复错误,极大的增加了开发效率。
要在 VSCode 中集成 ESLint 检查,我们需要先安装 ESLint 插件,点击「扩展」按钮,搜索 ESLint,然后安装即可。
VSCode 中的 ESLint 插件默认是不会检查 .ts
后缀的,需要在「文件 => 首选项 => 设置 => 工作区」中(也可以在项目根目录下创建一个配置文件 .vscode/settings.json
),添加以下配置:
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript"
],
"typescript.tsdk": "node_modules/typescript/lib"
}
这时再打开一个 .ts
文件,将鼠标移到红色提示处,即可看到这样的报错信息了
我们还可以开启保存时自动修复的功能,通过配置:
{
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
},
],
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
就可以在保存文件后,自动修复为:
let myName = 'Tom';
interface Foo {}
使用 Prettier 修复格式错误
ESLint 包含了一些代码格式的检查,比如空格、分号等。但前端社区中有一个更先进的工具可以用来格式化代码,那就是 Prettier。
Prettier 聚焦于代码的格式化,通过语法分析,重新整理代码的格式,让所有人的代码都保持同样的风格。
首先需要安装 Prettier:
npm install --save-dev prettier
然后创建一个 prettier.config.js
文件,里面包含 Prettier 的配置项。Prettier 的配置项很少,这里我推荐大家一个配置规则,作为参考:
// prettier.config.js or .prettierrc.js
module.exports = {
// 一行最多 100 字符
printWidth: 100,
// 使用 4 个空格缩进
tabWidth: 4,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾不需要逗号
trailingComma: 'none',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf'
};
使用 AlloyTeam 的 ESLint 配置
ESLint 原生的规则和 @typescript-eslint/eslint-plugin
的规则太多了,而且原生的规则有一些在 TypeScript 中支持的不好,需要禁用掉。
这里我推荐使用 AlloyTeam ESLint 规则中的 TypeScript 版本,它已经为我们提供了一套完善的配置规则,并且与 Prettier 是完全兼容的(eslint-config-alloy 不包含任何代码格式的规则,代码格式的问题交给更专业的 Prettier 去处理)。
安装:
npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-alloy
在你的项目根目录下创建 .eslintrc.js
,并将以下内容复制到文件中即可:
module.exports = {
extends: [
'alloy',
'alloy/typescript',
],
env: {
// 您的环境变量(包含多个预定义的全局变量)
// Your environments (which contains several predefined global variables)
//
// browser: true,
// node: true,
// mocha: true,
// jest: true,
// jquery: true
},
globals: {
// 您的全局变量(设置为 false 表示它不允许被重新赋值)
// Your global variables (setting to false means it's not allowed to be reassigned)
//
// myGlobal: false
},
rules: {
// 自定义您的规则
// Customize your rules
}
};
更多的使用方法,请参考 AlloyTeam ESLint 规则
使用 ESLint 检查 tsx 文件
安装 eslint-plugin-react
npm install --save-dev eslint-plugin-react
package.json 中的 scripts.eslint 添加 .tsx
后缀
{
"scripts": {
"eslint": "eslint src --ext .ts,.tsx"
}
}
VSCode 的配置中新增 typescriptreact 检查
{
"files.eol": "\\n",
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.autoFixOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
],
"typescript.tsdk": "node_modules/typescript/lib"
}