TypeScript快速上手

简介

参考文档

  • 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))

然后重新执行编译命令,发现报如下错误

TypeScript快速上手_第1张图片

但是 hello.js 文件仍然正常生成

function sayHello(person) {
    return 'Hello' + person;
}
var user = ['Tom', 'Jack'];
console.log(sayHello(user));

这是因为 ts 只会在编译时报错,但是在运行时不会提示错误

基础

原始数据类型

JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。

原始数据类型包括:布尔值、数值、字符串、nullundefined 以及 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 中,可以使用 nullundefined 来定义这两个原始数据类型

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 来实现这个接口,实现接口的对象的形状必须和接口一致,多一个属性或者少一个属性都不行

多一个时:

TypeScript快速上手_第2张图片

少一个时:

TypeScript快速上手_第3张图片

可见,赋值的时候,变量的形状必须和接口的形状保持一致

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性。

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 兼容 BB 兼容 A 即可

声明文件

新语法索引
  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • 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):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 CatDog 都继承自 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')

也可以不手动指定泛型,让类型推断自动推断出来数据类型

TypeScript快速上手_第4张图片

多个类型参数
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,会出现如下错误

TypeScript快速上手_第5张图片

需要注意的是,我们使用的是 ./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 文件,将鼠标移到红色提示处,即可看到这样的报错信息了

8558467.jpg

我们还可以开启保存时自动修复的功能,通过配置:

{
    "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"
}

你可能感兴趣的:(javascript,typescript,javascript,前端)