温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

有哪些关于TypeScript的知识点

发布时间:2021-10-28 15:42:18 来源:亿速云 阅读:199 作者:iii 栏目:web开发

这篇文章主要介绍“有哪些关于TypeScript的知识点”,在日常操作中,相信很多人在有哪些关于TypeScript的知识点问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”有哪些关于TypeScript的知识点”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

开始 TypeScript

学习准备

开始 TypeScript(以下简称 TS)正式学习之前,推荐做好以下准备:

  • Node 版本 > 8.0

  • IDE(推荐 VS Code,TS 是微软推出的,VS Code 也是微软推出,且轻量。对 TS 代码更友好)

有哪些关于TypeScript的知识点

初识 TypeScript

打开 TypeScript 官网可以看到官方对 TS 的定义是这样的

JavaScript and More A Result You Can TrustGradual Adoption

这三个点就很好地诠释了 TypeScript 的特性。在此之前,先来简单体验下 TypeScript 给我们的编程带来的改变。

这是一个 .js 文件代码:

let a = 123a = '123'

这是 .ts 文件代码:

let b = 123b = '123'

当我们在 TS 文件中试图重新给 b 赋值的时候,发生了错误,鼠标移动到标红处,系统提示:

Type ·"123"' is not assignable to type 'number'

原因是什么呢?

答案很简单,在 TS 中所有变量都是静态类型,let b = 123 其实就是 'let b:number = 123'。b 只能是 number  类型的值,不能赋值给其他类型。

TypeScript 的优势

  • TS 静态类型,可以让我们在开发过程中发现问题

  • 更友好的编辑器自动提示

  • 代码语义清晰易懂,协作更方便

配上代码来好好感受下这三个优势带给我们的编程体验有多直观,建议边在编辑器上敲代码。

先上最熟悉的 JS:

function add(data) {     return data.x + data.y }add()  //当直接这样写,在运行的时候才会有错误告知 add({x:2,y:3})

再上一段 TS 代码(如果对语法有疑问可以先不纠结,后续会有讲解,此处可以先带着疑问)

interface Point { x: number, y: number } function tsAdd(data: Point): number {     return data.x + data.y }tsAdd()  //直接这样写,编辑器有错误提示 tsAdd({ x: 1,y: 123})

当我们在 TS 中调用 data 变量中的属性的时候,编辑器会有想 x、y 属性提示,并且我们直接看函数外部,不用深入,就能知道 data  的属性值。这就是 TS 带给我们相比于 JS 的便捷和高效。

TypeScript 环境搭建

搭建 TypeScript 环境,可以直接在终端执行命令:

npm install -g typescript

然后我们就可以直接 cd 到 ts 文件夹下,在终端运行:

tsc demo.ts

tsc 简而言之就是 typescript complaire,对 demo.ts 进行编译,然后我们就可以看到该目录下多了一个同名的 JS  文件,可以直接用 Node 进行编译。

到这里我们就可以运行 TS 文件了,但是这只是一个文件,而且还要先手动编译成 TS 在手动运行 Node,有没有一步到位的命令呢?当然有,终端安装  ts-node:

npm install -g ts-node

这样我们可以直接运行:

ts-node demo.ts

来运行 TS 文件,如果要初始化 ts 文件夹,进行 TS 相关配置,可以运行:

tsc --init

关于相关配置,这里我们先简单提下,后面将会分析常用配置,可以先自行打开 tsconfig.json  文件,简单看下其中的配置,然后带着疑问继续往下看。

再理解下 TypeScript 中的 Type

正式介绍 TS 的语法之前,还需要再把开篇提到的静态类型再来说清楚一些。

const a: number = 123

之前说过,代码的意思是 a 是一个 number 类型的常量,且类型不能被改变。这里我要说的深层意思是,a 具有 number  的属性和方法,当我们在编辑器调用 a 的属性和方法的时候,编辑器会给我们 number 的属性和方法供我们选择。

有哪些关于TypeScript的知识点

TS 不仅允许我们给变量定义基础类型,还可以定义自定义类型:

interface Point {     x: number     y: number } const a: Point = {     x: 2,     y: 3 }

把 a 定义为 Point 类型,a 就拥有了 Point 的属性和方法。而我们把 a 定义为 Point 类型之后,a 必须 Point 上 的 x 和  y 属性。这样我们就把 Type 理解的差不多了。

TypeScript 的类型分类

类比于 JavaScript 的类型,TypeScript 也分为基础类型和引用类型。

原始类型

原始类型分为 boolean、number、string、void、undefined、null、symbol、bigint、any、never

JS 中也有的这里就不多解释,主要说下之前没有见过的几种类型,但是需要注意一下的是我们在声明 TS  变量类型的时候都是小写,不能写成大写,大写是表示的构造函数。

void 表示没用任何类型,通常我们会将其赋值给一个没有返回值的函数:

function voidDemo(): void {     console.log('hello world') }

bigint 可以用来操作和存储大整数,即使这数已经超出了 JavaScript 构造函数 Number  能够表示的安全整数范围,实际场景中使用较少,有兴趣的同学可以自行研究下。

any 指的是任意类型,在实际开发中应该尽量不要将对象定义为 any 类型:

let a: any = 4 a = '4'

never 表示永不存在的值的类型,最常见的就是函数中不会执行到底的情况:

function error(message: string): never {     throw new Error(message)     console.log('永不执行') }function errorEmitter(): never {     while(true){} }

引用类型

对象类型:赋值时,内必须有定义的对象属性和方法

const person: {     name: string     age: number } = {     name: 'aaa'     age: 18 }

数组类型:数组中每一项都是定义的类型。

const numbers: number[] = [1, 2, 3]

类类型:可以先不关注写法,后面还会详细讲解。

class Peron {} const person: Person = new Person()

类型的介绍差不多就这么些知识点,先在脑海里有个印象,不懂的地方可以继续带着疑问往下看。

TypeScript 类型注解和推断

之前已经讲过 TypeScript 的类型和它的类型种类,这一小节还是想继续把有关类型的知识讲全,那么就是类型注解和类型推断。

类型注解

let a: number a = 123

上面代码中这种写法就是类型注解,通过显式声明,来告诉 TS 变量的类型:

let b = 123

这里我们并没有显式声明 b 的类型,但是我们在编辑器中把光标放在 b 上,编辑器会告诉我们它的类型。这就是类型推断。

简单的情况,TS 是可以自动分析出类型,但是复杂的情况,TS 无法分析变量类型,我们就需要使用类型注释。

// 场景一 function add(first,second) {     return first + second }const sum = add(1,2) // 场景二function add2(first: nnumber,second: number) {     return first + second }const sum2 = add2(1,2)

在场景一中,形参 first、second 的类型 TS 推断为 any,且函数的返回值也是推断为 any,因为这种情况下,TS  无法判断类型,传参的时候可能传 number 或者 string 等。

场景二中,即使我们没有定义 sum2 的类型,TS 一样可以推断出 number,这是因为 sum2 是由 first second  求和的结果,所以它一定是 number。

不管是类型推断还是类型注解,我们的目的都是希望变量的类型是固定的,这样不会把 typescript 变成 anyscript。

补充:函数结构中的类型注解。

// 情况一  function add({ first }: {first: number }): number {     return first } // 情况二 function add2({first, second}: {first: number, second: number}): number {     return first + second } const sum2 = add({ first: 1, second: 2}) const sum2 = add2({ first: 1, second: 2})

TypeScript 进阶

配置文件

之前我们提到过,当我们要运行 TS 文件时,执行命令 tsc 文件名 .ts 就可以编译 TS 文件生成一个同名 JS  文件,这个过程是怎么来的呢,或者如果我们想修改生成的文件名和文件目录该怎么办呢?

相信你已经心里有答案了,没错,和 webpack 打包或者 babel 编译一样,TS 也有一个编译配置文件 tsconfig.json。当我们执行ts  --init,文件目录下就多了一个 TS 配置文件,TS 编译成 js,就是由 tsconfig 中配置而来。

为了验证下 tsconfig 文件确实会对 TS 文件编译做配置,修改里面的:

"removeComments": true //移除文件中的注释

然后新建一个 demo.ts 文件:

// 这是一个注释 const a: number = 123

执行 tsc demo.ts,打开 demo.js 文件,发现注释并没有被移除,这是怎么回事,配置文件不生效?

真相是这样的,当我们直接执行文件的时候,并不会使用 tsconfig 中的配置,只有我们直接执行 tsc,就会使用 tsconfig 中的配置,直接运行  tsc,你就发现了,amazing!

当运行 tsc 命令的时候,直接会先去找到 tsconfig 配置文件,如果没有做其他改动,会默认编译根目录下的 TS 文件。

如果想编译指定文件,则可以在 compilerOptions 配置项同级增加:

"include": ["./demo.ts"]或者"files": ["./demo.ts"]

如果想要不包含某个文件,则可以同上增加:

"exclude": ["./demo.ts"]

有关于这一块的更多配置,可以参考 tsconfig 配置文档。

下面再来关注下 compilerOptions 中的属性,由这个英文名就知道,这其实就是指的编译配置的意思。

"compilerOptions": {     "increments": true                          // 增量编译,只编译新增加的内容     "target": "es5",                            // 指定 ECMAScript 目标版本: 'ES5'     "module": "commonjs",                       // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'     "moduleResolution": "node",                 // 选择模块解析策略     "experimentalDecorators": true,             // 启用实验性的ES装饰器     "allowSyntheticDefaultImports": true,       // 允许从没有设置默认导出的模块中默认导入。     "sourceMap": true,                          // 把 ts 文件编译成 js 文件的时候,同时生成对应的 map 文件     "strict": true,                             // 启用所有严格类型检查选项     "noImplicitAny": true,                      // 在表达式和声明上有隐含的 any类型时报错     "alwaysStrict": true,                       // 以严格模式检查模块,并在每个文件里加入 'use strict'     "declaration": true,                        // 生成相应的.d.ts文件     "removeComments": true,                     // 删除编译后的所有的注释     "noImplicitReturns": true,                  // 不是函数的所有返回路径都有返回值时报错     "importHelpers": true,                      // 从 tslib 导入辅助工具函数     "lib": ["es6", "dom"],                      // 指定要包含在编译中的库文件     "typeRoots": ["node_modules/@types"],     "outDir": "./dist",                         // 生成文件目录     "rootDir": "./src"                          // 入口文件   },

接口(interface)

接口是用来自定义类型或者为我们的第三方 JS  库做翻译的一种方式。之前的代码中已经使用到了接口,其实就是用来描述类型的。每个人都有姓名和年龄,那我们就会这样去约束 person。

interface Person {     name: string     age: number }let person: Person

当我们进行这样的类型约束的时候,person 这个对象在初始化的时候就必须要有 name 和 age,初始化有两种方式,再来看下其中的不同支出:

// 承接上面的代码 // 第一种初始化方式 person = {     name: 'aaa',     age: 18 } // 第二种初始化方式 let p = {     name: 'aaa',     age: 18,     sex: 'male' } person = p

第一种方式和第二种方式相比,p 对象中多了一个 sex 属性,然后赋值给了 person,编辑器没有提示错误,但是如果在第一种方式中添加一个 sex  属性则会报错,这是为什么呢?

这是因为,当我们直接赋值(也就是通过第一种方式)的时候,TS 会进行强类型检查,因此必须和接口定义的类型一致才行。

注意我们上面提到一致,一致的意思是,属性名和属性值类型一致,且属性个数不多不少。而当使用第二种方式进行赋值的时候,则会进行弱检查。属性个数一致会较弱,表现在,当属性多了一个的时候,不会有语法错误。

此时我们会产生一个疑问,如果我们想让第一种方式也能做到和第二种方式一样,或者说,每个人年龄和姓名是必须的,但是所在城市 city  是选填的,那该如何呢?我们可以用可选属性描述。

interface Person {     name: string     age: number     city?: string }

如果这样的话,我们在调用 p 属性的时候就可以看到 city 属性可能是 string,也可能是 undefined:

有哪些关于TypeScript的知识点

不仅如此,我们还希望,age 属性是不可修改的,readonly 属性自然就派上用场了,当你试图修改定义了 readonly  属性的时候,那么编辑器就会发出警告:

interface Person {     name: string     readonly age: number     city?: string }let person: Person = {     name: 'aaa',     age: 18 }// person.age = 18
有哪些关于TypeScript的知识点

当然这还没结束,如果有一天,还想再扩展一个接口,是公司职员的接口,但是职员接口类肯定有 Person 类的所有信息,再扩展一个  id,又该如何呢?这时候继承(extends)就上场了。

interface Employee extends Person {     id: number }

接口还可以用来约束类,让定义的类必须有某种属性或者方法,这时候关键字就不是 extends,而是 implements。

interface User {   name: string  getName(): string}class Student implements User {   name = 'aaa'   getName() {    return this.name   }}

interface VS type

interface 和 type 作用看起来似乎是差不多的,都是用来定义类型,接下让我们看下它的相同点与不同点。

相同点:

1. 都可以描述对象或函数

interface Person {   name: string   age: number}type Person1 = {  //type 定义类型有等号   name: string   age: number}interface getResult {  (value: string): void }type getResult1 = (value: string): void

2. 都可以实现继承

// interface 继承 interface interface People extends Person {  sex: string }// interface 继承 typeinterface People extends Person1 {  sex: string }// type 继承 type type People1 = Person1 & {  sex: string }// type 继承 interface type People1 =  Person & {  sex: string }

不同点:

1. type 可以声明基本类型、联合类型,interface 不行

// 基本类型   type Person = string // 联合类型type User = Person | number

2. interface 可以类型合并

interface People {   name: string   age: number }interface People {   sex: string }//People 最终类型为 {   name: string   age: number   sex: string }

3. interface 可以定义可选和只读属性(之前讲过,这里不再赘述)

接口的基础知识差不多就介绍完了,当然接口在实际开发场景中应用会更复杂,如果你还有很多疑惑,接着往下看,下面的讲解将会解答你的疑惑。

联合类型和类型保护

和其他分享资料不同,我希望每一个知识点都能先让你先有所疑惑,启发你的思考,然后我再慢慢解决你的疑惑,这样我相信你会记忆更加深刻,否则可能将成效见微。

闲话少叙,直接上一段代码:

interface Bird {   fly: boolean   sing: () => {} }interface Dog {   fly: boolean   bark: () => {} }function trainAnimal(animal: Bird | Dog) {   // animal.sing() }

上面代码中我定义了两个类型,一个 Bird 类型,一个是 Dog 类型。函数 trainAnimal 的形参接收一个 animal 的参数,这个参数可能是  Bird 类型,也可能是 Dog 类型,这就是联合类型。当在函数中调用的时候,编辑器给的提示只有 fly:

有哪些关于TypeScript的知识点

这还真有点东西,但是仔细想想,就觉得只有 fly 没毛病。因为联合类型的 animal  无法确定具体是哪个类型,因此只能提示共有的属性。而独有方法经过联合类型阻隔之后是无法进行语法提示。如果我们强行调用某个类型独有的方法,可以看到编辑器会有错误提示。

有哪些关于TypeScript的知识点

如果确实需要使用独有方法,该当如何?

这就需要类型保护了,确实,如果联合类型只能调用共有方法,似乎看起来也用处不是很大,好在有类型保护。类型保护也有好多种,我们分别来介绍下。

1. 类型断言

function trainAnimal(animal: Bird | Dog) {     if (animal.fly) {         (animal as Bird).sing()     } else {         (animal as Dog).bark()     }}

上面代码中通过一个 as 关键字实现了类型断言。因为按照逻辑,我们知道,如果有 fly 方法,那么 animal 一定是 Bird  类型,但是编辑器不知道,所以通过 as 告诉编辑器此时 animal 就是 Bird 类型,Dog 类型的确定也是同理。

2. 通过 in 来类型断言,TS 语法检查就能确定参数类型

function trainAnimalSecond(anmal: Bird | Dog ) {     if ('sing' in animal) {         animal.sing()     }}

3. 通过 typeof 来做类型保护

function add(first: string | number, second: string | number) {     if (typeof first === 'string' || typeof second === 'string') {         return `first:${first}second:${second}`     }    return first + second }

上面代码中如何没有 if 里面的逻辑,直接进行判断,编辑器则会给错,因为如果是数字和字符串相加,则可能存在错误,因此通过 typeof 来确定,当  first 和 second 都是数字的时候,进行相加。

4. 通过 instanceof 来类型保护

class NumberObj {     count: number}function addSecond(first: object | NumberObj, second: object | NumberObj) {     if (first instanceof NumberObj && second instanceof NumberObj) {         return first.count + second.count     }}

在 TS  中,类不仅可以用来实例化对象,也可以用来定义变量类型,当一个对象被一个类定义以后,表明这个对象的值就是这个类的实例,关于类这一块的写法有疑问,可以查阅下 ES7  相关内容,这里不做过多讲解。

从代码中我们可以看出,通过 instanceof 来确定具有联合类型的形参是否是类的类型,当然这里如果要用 instanceof  来判断,我们的自定义类型定义只能用 class。如果是 interface 定义的类型,使用 instanceof 则会报错。

枚举类型

枚举这个概念,我们在 JS 中就已经接触的比较多了,关于概念也不就不做过多的讲解,直接上一段代码。

const Status = {     OFFLINE: 0,     ONLINE: 1,     DELETED: 2 }function getStatus(status) {     if (status == Status.OFFLINE) {         return 'offline'     } else if (status == Status.ONLINE) {         return 'online'     } else if (status == Status.DELETED) {         return 'deleted'     }    return error }

这是我们在 JS 中比较常见的写法,TS 中也有枚举类型,而且比 JS 的更好用。

enum Status {     OFFLINE,    ONLINE,    DELETED}// 方式一 const status = Status.OFFLINE  // 0 // 方式二 const status = Status[0]  // OFFLINE

通过上面的代码可以看出,TS 的枚举类型默认会有赋值,而且写法也很简单。再看方式一和方式二对枚举类型的使用,我们可以看出,TS  枚举类型还支持正反调用。

刚才说到枚举类型默认有值,如果我想改默认值又该如何呢?请看下面的代码:

enum Status {     OFFLINE = 3,     ONLINE,    DELETED}const status = Status.OFFLINE  // 3 const status = Status.ONLINE  // 4 enum Status1 {    OFFLINE = 6,     ONLINE = 10,     DELETED}const status = Status.OFFLINE  // 6 const status = Status.ONLINE  // 10 const status = Status.DELETED  // 11

由上可以看出,TS 枚举类型支持自定义值,且后面的枚举属性没有赋值的话,会在原来的基础上递增。

上面我们说到 enum 支持双向使用,为什么它如此之秀,怎么灵活呢,我们看下枚举类型编译成 JS 后的代码:

var Status; (function (Status) {     Status[Status["OFFLINE"] = 6] = "OFFLINE";     Status[Status["ONLINE"] = 10] = "ONLINE";     Status[Status["DELETED"] = 12] = "DELETED"; })(Status || (Status = {}))

函数泛型

泛型在 TS 的开发中使用非常广泛,因此这一节,同样会由浅入深,先看代码:

function result(first: string | number, second: string | number) {     return `${first} + ${second}` }join('1', 1) join(1,'1')

这是我们之前讲过的联合类型,两个参数既可以是数字也可以字符串。

但是现在我有个需求是这样的,如果 first 是字符串,则 second 只能是字符串,同理 first 是数字,则  second。如果不知道泛型,我们只能在函数内部去进行逻辑约定,但是泛型一出手,问题就迎刃而解。

function result<T>(first: T,second: T) {     return `${first} + ${second}` }join<number>(1,1) join<string>('1','1')

通过在函数中定义一个泛型 T(名字可以自定义,一般用 T),这样的话,我们就可以约束 first,second  类型一致,当我们试图调用的时候实参类型不一致的时候,那么编辑器就会报错。

function map<T>(params: T[]) {     return params }map([1])

这种形式也是可以的,虽然调用的时候没有显示定义 T,但是 TS 可以推断出 T 的类型。T[] 是数组一种定义类型的方式,表明数组每个值的类型。

注意:Array 这种形式在 3.4 之后,会有警告。统一使用方括号形式。

这是单一泛型,但实际场景中往往是多个泛型:

function result<T, U>(first: T,second: U) {     return `${first} + ${second}` }join<number,string>(1,'1') join(1, '1')  //这种形式也可

泛型如此之好用,肯定不可能只在函数中使用,因此接下来再来说下类中使用泛型:

class DataManager {   constructor(private data: string[] | number[]) {}   getItem(index: number): string | number {     return this.data[index]   }}const data = new DataManager([1]) data.getItem(0)

DataManager 类中构造函数通过联合类型来定义 data  的类型,这在复杂的业务场景中显然是不可取的,因为如果我们也不确定类型,在传参之前,那么只能写更多的类型或者定义成 any  类型,这就显得很不灵活,这时候我们想到了泛型,是否可以应用到类中呢?

答案是肯定的。

class DataManager<T> {   constructor(private data: <T>) {}   getItem(index: number): <T> {    return this.data[index]   }}const data = new DataManager([1]) // const data = new DataManager<number>([1])  //直观的写法,和上面等价 data.getItem(0)

看起来好像已经很灵活了,但是还有一个问题,没有规矩不成方圆,函数编写者允许调用者具有传参灵活度,但是需要符合函数内部的一些逻辑,也就是说之前函数  return this.data[index],但是现在函数逻辑里面,返回的是 this.data[index].name,也就是函数调用者可以传 T  类型进来,但是每一项必须要有 name 属性,这又该当如何?

那么我们可以再定义一个接口,让 T 继承接口,这样既能保持灵活度,又能符合函数逻辑。

interface Item {     name: string }class DataManager<T extends Item> {     constructor(private data: T[]) {}     getItem(index: number): number {         return this.data[index].name     }}const data = new DataManager([     name: 'dell' ])

讲到这里,泛型差不多结束了,但是还有一个疑问,上面number | string 这种联合类型想用泛型来约束,该怎么写呢,也就是 T 只能是 string  或者 number。

class DataManager<T extends number | string> {     constructor(private data: T[]) {}     getItem(index: number): T {         return this.data[index]     }}

命名空间

讲到这里,我们之前已经新建了很多的 demo 文件,不知道你有没有发现这样一个奇怪的现象。

demo.ts

let a = 123// dosomething

demo1.ts

let a = '123'

当我们在 demo1.ts 文件中再去定义 a 这个变量的时候,a 会标红,告诉我们 a 已经被声明了 number 类型,这是为什么呢?

我们明明在 demo1.ts 文件中没有定义过 a,再仔细看下提示,它告诉我们已经在 demo.ts 中定义过了。对 JS  很熟练的伙伴一定知道了,应该是模块化的问题。

没错,TS 跟 JS 一样,一个文件中不带有顶级的 import 或者 export 声明,它的内容是全局可见的,换句话说,如果我们文件中带有  import 或者 export,则是一个模块化。

export const let a = '123'

这样就没有问题了,我们再看下下面这段代码:

class A {   // do something } class B {   // do something } class C {   // do something } class D {   constructor() {     new A()     new B()     new C()   } }

代码中,我定义了四个类,上面提到,如果我把 D 这个类通过 export 导出,这样其他文件中就可以继续使用 A  或者其他几个类名了,但是我现在有个需求是这样的,我不想把 A、B、C 三个类暴露出去,而且在外面能不能通过想通过对象的方式去调用 D 这个类。namespace  登场,看下代码:

namespace Total{   class A {     // do something   }   class B {     // do something   }   class C {     // do something   }   export class D {     constructor() {       new A()       new B()       new C()     }   } } Total.D

这样写就可以了,通过 namespace 就只能调用到 D。如果还想调用其他类,只需要在前面去 export 这个类就好了。

namespace 在实际开发中,我们一般用在写一些 .d.ts 文件。也就是 JS 解释文件。

命名空间本质上是一个对象,它的作用就是将一系列相关的全局变量变成一个对象的属性,再看下上面的代码编译成 JS 是怎么样的。

var Total; (function (Total) {     var A = /** @class */ (function () {         function A() {         }        return A;     }());    var B = /** @class */ (function () {         function B() {         }        return B;     }());    var C = /** @class */ (function () {         function C() {         }        return C;     }());    var D = /** @class */ (function () {         function D() {             new A();             new B();             new C();         }        return D;     }());    Total.D = D;})(Total || (Total = {}));Total.D;

从上面可以看出,通过一个立即执行函数并且传了一个变量进去,然后把导出的方法挂载在变量上,这样就可以在外面通过对象属性的方式调用类。

最后再补充下 declare,它的作用是,为第三方 JS 库编写声明文件,这样才可以获得对应的代码补全和接口提示:

//常用的声明类型   declare function 声明全局方法 declare class 声明全局类 declare enum 声明全局枚举类型 declare global 扩展全局变量 declare module 扩展模块

也可以使用 declare 做模块补充。下面摘自官方的一个示例:

// observable.ts export class Observable<T> {    // ... implementation left as an exercise for the reader ... }// map.tsimport { Observable } from "./observable"; declare module "./observable" {     interface Observable<T> {        map<U>(f: (x: T) => U): Observable<U>;    }}Observable.prototype.map = function (f) {     // ... another exercise for the reader }// consumer.tsimport { Observable } from "./observable"; import "./map"; let o: Observable<number>; o.map(x => x.toFixed());

代码的意思在 map.js 中定制一个文件,补充你想要的类型 map 方法并实现函数挂载在 Observable 原型上,然后在 consumer.ts  就可以使用 Observable 类型里面的 map。

TypeScript 高级语法

类的装饰器

装饰器我们在 JS 就已经接触比较久了,并且在我的另一篇  Chat《写给前端同学容易理解并掌握的设计模式》中也详细讲解了装饰器模式,对设计模式感兴趣的同学,欢迎订阅。装饰器本质上就是一个函数。@description  这种语法其实就是一个语法糖。TS 和 JS 装饰器使用大同小异,先看一个简单的例子:

function Decorator(constructor: any) {     console.log('decorator') }@Decorator class Demo{} const text = new Test()

当我们觉得完美的时候,编辑器给了我们一个标红:

有哪些关于TypeScript的知识点

其实装饰器是一个实验性质的语法,所以不能直接使用,需要打开实验支持,修改 tsconfig 的以下两个选项:

"experimentalDecorators": true, "emitDecoratorMetadata": true,

修改完配置之后,就发现终端正确输出了。

但是这里我还要再抛出一个问题,装饰器的运行时机是什么时候呢,是在类实例化的时候吗?

其实装饰器在类创建的时候就已经运行装饰器了,可以自行注释掉实例化语句,再运行,看控制台是否有 log。

类的装饰器修饰函数接受的参数是类的构造函数,我们可以改一下 Decorator 来验证一下:

function Decorator(constructor: any) {     constructor.prototype.getResult = () => {         console.log('constructor')     }}@Decorator class Demo{} const text = new Test() text.getResult()

控制台正确打印出 constructor  就可以证明接收的参数确实是类的构造函数。上面的代码中我们只在类中使用了一个装饰器,但其实可以给一个类使用多个装饰器,写法如下:

@Decorator @Decorator1 class Demo{}

多个装饰器执行顺序为先下后上。

上面的装饰器写法,我们把整个函数都给了类做装饰,但是实际情况是,我函数有一些逻辑,是不给类装饰使用的,那么我们写成一个工厂模式去给类装饰:

function Decorator() {     // do something     return function (constructor: any) {         console.log('descorator')     }}@Decorator()class Test()

通过这样,我们可以传一些参数进去,然后函数内部去控制装饰器的装饰。

不知道你有没有发现,我们在验证装饰器参数的时候,当我们通过类的实例去调用我们挂载在装饰器原型的方法的时候,虽然没有报错,但是编辑器没有给我们提示,这是很不符合我们预期的。上面那种装饰器写法很简单,但很直观。

但在 TS 中我们往往是像下面这种方式使用的,而且也能解决上面提到的那个问题:

function Decorator() {     return function <T extends new (...args: any[]) => any>(constructor: T) {         return class extends constructor{             name = 'bbb'             getName        }    }} const Test = Decorator()( class {     name: string     constructor(name: string) {         console.log(this.name,'1')         this.name = name         console.log(this.name,'2')     }})const test = new Test('aaa') console.log(test.getName())

我们把之前的代码大变样,看起来似乎高大上了许多,但是理解起来也挺有难度的。别急,让我来一一进行解释。

<T extends new (...args: any[]) => any>

这个是一个泛型,T 继承了一个构造函数也可以说是继承了一个类,构造函数参数是一个展开运算符,表示接收多个参数。

这样泛型 T 就可以用来定义 constructor。而 Decorator  函数,跟上面一样,我们写成函数柯里化形式,并且把类作为参数传递进去,摒弃了之前的语法糖,这样我们在调用装饰在类上的方法的时候编辑器就能给我们提示。

方法装饰器

上一节,分享完了类的装饰器,大家肯定对装饰器意犹未尽,这一小节,再分享下给类的方法装饰,先上个代码,来看下:

function getNameDecorator(   target: any,   key: string,   descriptor: PropertyDescriptor ) {   console.log(target); } class Test {      name: string      constructor(name: string) {          this.name = name      }     @getNameDecorator      getName() {         return this.name      } }const test - new Test('aaa') console.log(test.getName())

这就实现了给类的方法进行装饰,当我们给类的普通方法进行装饰的时候,装饰器函数中接收的参数 target 对应的是类的 prototype,key  是装饰的普通方法的名字。

注意,我上面说的是普通方法。和类的装饰器一样,方法装饰器的执行时机同样是当方法被定义的时候。

刚才我已经强调了普通方法,接下来我就要说静态方法了。

class Test {     name: string     constructor(name: string) {         this.name = name     }    @getNameDecorator     static getName() {         return this.name     }}

静态方法的装饰器函数中,第一个参数 target 对应的是类的构造函数。

类的方法装饰器函数中,我们还有一个参数没有讲,那就是 descriptor。

不知道你有没有发现,这个函数接收三个参数,而且第三个参数还是 descriptor,有点像 Object.defineProperty 这个  API,当我们在函数中调用 descriptor 的时候,编辑器会给我们提示。

这几个属性和 Object.defineProperty 中的 descriptor  可设置属性一样,没错,功能也是一样的.比如,我们不想在外部,getName 方法被重写,那么我们可以这样:

function getNameDecorator(   target: any,   key: string,   descriptor: PropertyDescriptor ) {   console.log(target);   descriptor.writable = false }

当你试图这样去修改它的时候,运行编译后文件将会报错:

const test = new Test('aaa') console.log(test.getName()) test.getName = () => {     return 'aaa' }

这是运行结果:

有哪些关于TypeScript的知识点

访问器装饰器

在 ES6 的 class 中新增访问器,通过 get 和 set 方法访问属性,如果上面的知识点你都消化了,那么访问器装饰器的用法也是如出一辙。

function visitDecorator(     target: any,     key: string,     descriptor: PropertyDescriptor ){} class Test {     provate _name: string     constructor(name: string) {         this._name = name     }    get name() {         return this._name     }    @visitDecorator     set name() {         this._name = name     }}

访问器装饰器的用法跟类的普通方法装饰器用法差不多,这里就不展开来讲了。同样地,在类中,我们也可以给属性添加装饰器,参数添加装饰器。

装饰器业务场景使用

之前我们花了比较长的篇幅来介绍装饰器,这一小节,将跟大家分享下实际业务场景中,装饰器的使用。首先来看这样一段代码:

const uerInfo: any = undefined class Test {    getName() {        return userInfo.name     }    getAge() {        return userInfo.name     }}const test = new Test() test.getName()

这段代码不用运行,我们都能知道,会报错,因为 userInfo 没有 name 属性。因此如果我们想要不报错,就会写成这样:

class Test {     getName() {        try {             return userInfo.name         } catch (e) {             console.log('userInfo.name 不存在')         }    }    getAge() {        try {             return userInfo.age         } catch (e) {             console.log('userInfo.age 不存在')         }    }}

把类改成这样,似乎就没有问题了,为什么说似乎呢?

那是因为运行虽然没有问题,但是如果我们还有很多类似于这样的方法,我们是否要重复处理错误呢?能否用到之前讲的装饰器来处理错误:

const userInfo: any = undefined function catchError(    target: any,     key: string,     descriptor: PropertyDescriptor){    const fn = descriptor.value     descriptor.value = function() {         try {             fn()        } catch (e) {             console.log('userinfo 出问题啦')         }    }}class Test {     @catchError     getName() {        return userInfo.name     }    @catchError     getAge() {        return userInfo.age     }}

这样我们就把捕获异常的逻辑提取出来了,通过装饰器来复用。

但是和我们之前写的还有点差异,就是报错信息都一样,我们不知道具体是哪个函数报的错,也就是说,我们希望装饰器函数可以接收一个参数,来完善报错信息,这样的话,我们就可以用到讲过的,把装饰器包装成一个工厂函数,代码如下:

function catchError(msg: string) {     return function (         target: any,         key: string,         descriptor: PropertyDescriptor     ){         const fn = descriptor.value         descriptor.value = function() {             try {                 fn()            } catch (e) {                 console.log(`userinfo.${msg} 出问题啦`)             }        }    }}class Test {     @catchError('name')     getName() {        return userInfo.name     }    @catchError('age)'     getAge() {        return userInfo.age     }}

这样我们的代码就能满足我们的需求了,后面我们再添加其他函数函数,也可以用装饰器对其进行装饰。

项目中应用 TypeScript

脚手架搭建一个 TypeScript

现在的开发越来越专业,一般我们初始化一个项目,如果不用脚手架进行开发的话,需要自己去配置一大堆东西,比如  package.json、.gitignore,还有一些构建工具,像 webpack 等以及他们的配置。

而当我们去使用 TypeScript 编写一个项目的时候,还需要配置 TypeScript 的编译配置文件 tsconfig 以及 tslint.json  文件。

如果我们只是想做一个小项目或者只想学习这块的开发,那前期的磨刀准备工作将让很多人望而却步,一头雾水。因此,一个脚手架工具就可以帮我们把刀磨好,而且磨的铮鲜亮丽的,这个工具就是  TypeScript Library Starter。让我们一起来了解下。

查看它的官网,我们知道这是一个以 TypeScript 为基础的开源脚手架工具,帮助我们快速开始一个 TypeScript 项目,使用方法如下:

git clone https://github.com/alexjoverm/typescript-library-starter.git ts-project cd ts-projectnpm install

这几行命令的意思是,把代码拉下来然后给项目重命名。进入到项目,通过 npm install 去给项目安装依赖,然后我们来看下我们的文件目录:

有哪些关于TypeScript的知识点

├── package.json  // 项目配置文件 

├── rollup.config.ts // rollup 配置文件 ├── src // 源码目录 ├── test // 测试目录 ├── tools // 发布到 GitHup pages 以及 发布到 npm 的一些配置脚本工具 ├── tsconfig.json // TypeScript 编译配置文件 └── tslint.json // TypeScript lint 文件

TypeScript library starter  创建的项目确实集成了很多优秀的开源工具,包括打包、单元测试、格式化代码等,有兴趣的同学可以自行深入研究下。

还有需要介绍的是,TypeScript library starter 在 package.json 中帮我们配置了一套完整的前端工作流:

  • npm run lint:使用 TSLint 工具检查 src 和 test 目录下 TypeScript 代码的可读性、可维护性和功能性错误。

  • npm start:观察者模式运行 rollup 工具打包代码。

  • npm test:运行 Jest 工具跑单元测试。

  • npm run commit:运行 commitizen 工具提交格式化的 git commit 注释。

  • npm run build:运行 rollup 编译打包 TypeScript 代码,并运行 typedoc 工具生成文档。

其他一些命令在我们日常开发中使用不是非常多,有需要的同学可以再自行去了解。

TypeScript 实战

现在我们的前端项目基本都是使用框架进行开发,今天我就介绍如何使用 React + TypeScript 进行 React 项目开发。当然这里我们还是会使用  React 提供的脚手架迅速搭建项目框架,为了避免你本地之前的脚手架版本影响 TypeScript 的开发,建议先执行:

npm uninstall create-react-app

然后执行官方提供的 React TypeScript 生成命令:

npx create-react-app react-project --template typescript --use-npm

这个命令的意思是下载最新脚手架(如果当前环境没有这个脚手架的话),然后通过 create-react-app 脚手架去生成以 typescript  为开发模板的项目,项目名字叫 react-project,并通过 npm 去安装依赖,如果没有 --use-npm 则会默认是使用 Yarn。

项目搭建完成之后,我们把文件整理下,删除一些我们不用的文件,同时把相关引用也删除,最终文件目录如下:

有哪些关于TypeScript的知识点

当我们使用 TS 去写 React 的时候, jsx 就变成了 tsx。在 APP.tsx 文件中:

const App: React.FC = () => {     return <div className="App"></div> }

通过 React.FC 给函数定义了一个 React.FC 的函数类型,这是 React 中定义的函数类型。

前端 UI 开发,现在市面上也有很多封装好的框架,让我们可以快速搭建一个页面,这里我们选用 ant-design,这个框架也是使用 TypeScript  进行开发的,所以我们使用它进行开发的时候,会有很多类型可以供我们使用,因此使用它去巩固我们刚学习的 TypeScript 知识点会有更多的好处。

首先让我们来安装下这个组件库:

npm install antd --save

安装好之后,再 index.tsx 中引入 CSS 样式:

import 'antd/dist/antd.css'

接下来我们去写个登录页面,首页新建一个 login.css:

.login-page {   width: 300px;   padding: 20px;   margin: 100px auto;   border: 1px solid #ccc; }

然后我们去 antd-design 官网,把登录组件代码复制到我们的 App.ts 中:

import React from "react"; // import ReactDOM from 'react-dom' import "./login.css"; // function App() { //   return <div className="login-page">Hello world</div>; // } // export default App; import { Form, Input, Button, Checkbox } from "antd"; // import { Store } from "antd/lib/form/interface"; import { ValidateErrorEntity, Store } from "rc-field-form/lib/interface"; const layout = {  labelCol: {    span: 8,   },  wrapperCol: {    span: 16,   },};const tailLayout = {  wrapperCol: {    offset: 8,     span: 16,   },};const App = () => {   const onFinish = (values: Store) => {     console.log("Success:", values);   };  // const onFinishFailed = (errorInfo: Store) => {   const onFinishFailed = (errorInfo: ValidateErrorEntity) => {     console.log("Failed:", errorInfo);   };  return (     <div className="login-page">       <Form        {...layout}        name="basic"         initialValues={{          remember: true,         }}        onFinish={onFinish}        onFinishFailed={onFinishFailed}      >        <Form.Item          label="Username"           name="username"           rules={[            {              required: true,               message: "Please input your username!",             },          ]}        >          <Input />         </Form.Item>         <Form.Item           label="Password"           name="password"           rules={[             {               required: true,               message: "Please input your password!",             },           ]}         >           <Input.Password />         </Form.Item>         <Form.Item {...tailLayout} name="remember" valuePropName="checked">           <Checkbox>Remember me</Checkbox>         </Form.Item>         <Form.Item {...tailLayout}>           <Button type="primary" htmlType="submit">             Submit           </Button>         </Form.Item>       </Form>     </div>   ); }; // ReactDOM.render(<Demo />, mountNode); export default App;

其中,onFinish 函数的 values 编辑器给我们报隐患提示,我们也无法确定 value 的类型,但是又不能填写 any。因此,我们可以去找下  Form 中定义的类型。mac 用户把鼠标放在 import 中的 From 标签上( windows 用户按住  cmd),进入到源代码中去,然后一直去查找我们的方法的定义,首先我们进入到了:

有哪些关于TypeScript的知识点

然后 InternalForm 继承了 InternalForm,我们再继续去寻找,最后找到了源头:

有哪些关于TypeScript的知识点

同理我们也可以找到 onFinishFailed:

有哪些关于TypeScript的知识点

最后在文件中引入这两个类型即可。

经过上面的测试之后,我们的项目基本上就算已经搭建好了,接下来就可以继续充实相关的页面了。

这里再把文件整理下,把不需要的删除,src 目录下新建一个 pages 的目录,然后我们的页面组件都放在这里,把 login  的代码也在这个文件夹下新建一个文件存放,然后我们再修改下 App.ts:

import { Route, HashRouter, Switch } from "react-router-dom"; import React from "react"; import LoginPage from "./pages/login"; import Home from "./pages/home"; function App() {   return (     <div>       <HashRouter>         <Switch>         <Route path="/" exact component={Home}></Route>           <Route path="/login" exact component={LoginPage}></Route>         </Switch>       </HashRouter>     </div>   );}export default App;

由于 react-router-dom 是 JS 编写的文件,因此需要再安装一个类型定义文件:

npm install @types/react-router-dom -D

到此,关于“有哪些关于TypeScript的知识点”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI