这篇文章主要介绍“有哪些关于TypeScript的知识点”,在日常操作中,相信很多人在有哪些关于TypeScript的知识点问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”有哪些关于TypeScript的知识点”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
学习准备
开始 TypeScript(以下简称 TS)正式学习之前,推荐做好以下准备:
Node 版本 > 8.0
IDE(推荐 VS Code,TS 是微软推出的,VS Code 也是微软推出,且轻量。对 TS 代码更友好)
打开 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 环境,可以直接在终端执行命令:
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 文件,简单看下其中的配置,然后带着疑问继续往下看。
正式介绍 TS 的语法之前,还需要再把开篇提到的静态类型再来说清楚一些。
const a: number = 123
之前说过,代码的意思是 a 是一个 number 类型的常量,且类型不能被改变。这里我要说的深层意思是,a 具有 number 的属性和方法,当我们在编辑器调用 a 的属性和方法的时候,编辑器会给我们 number 的属性和方法供我们选择。
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 理解的差不多了。
类比于 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 的类型和它的类型种类,这一小节还是想继续把有关类型的知识讲全,那么就是类型注解和类型推断。
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})
配置文件
之前我们提到过,当我们要运行 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" // 入口文件 },
接口是用来自定义类型或者为我们的第三方 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:
不仅如此,我们还希望,age 属性是不可修改的,readonly 属性自然就派上用场了,当你试图修改定义了 readonly 属性的时候,那么编辑器就会发出警告:
interface Person { name: string readonly age: number city?: string }let person: Person = { name: 'aaa', age: 18 }// person.age = 18
当然这还没结束,如果有一天,还想再扩展一个接口,是公司职员的接口,但是职员接口类肯定有 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:
这还真有点东西,但是仔细想想,就觉得只有 fly 没毛病。因为联合类型的 animal 无法确定具体是哪个类型,因此只能提示共有的属性。而独有方法经过联合类型阻隔之后是无法进行语法提示。如果我们强行调用某个类型独有的方法,可以看到编辑器会有错误提示。
如果确实需要使用独有方法,该当如何?
这就需要类型保护了,确实,如果联合类型只能调用共有方法,似乎看起来也用处不是很大,好在有类型保护。类型保护也有好多种,我们分别来介绍下。
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。
类的装饰器
装饰器我们在 JS 就已经接触比较久了,并且在我的另一篇 Chat《写给前端同学容易理解并掌握的设计模式》中也详细讲解了装饰器模式,对设计模式感兴趣的同学,欢迎订阅。装饰器本质上就是一个函数。@description 这种语法其实就是一个语法糖。TS 和 JS 装饰器使用大同小异,先看一个简单的例子:
function Decorator(constructor: any) { console.log('decorator') }@Decorator class Demo{} const text = new Test()
当我们觉得完美的时候,编辑器给了我们一个标红:
其实装饰器是一个实验性质的语法,所以不能直接使用,需要打开实验支持,修改 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' }
这是运行结果:
访问器装饰器
在 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 去给项目安装依赖,然后我们来看下我们的文件目录:
├── 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 工具生成文档。
其他一些命令在我们日常开发中使用不是非常多,有需要的同学可以再自行去了解。
现在我们的前端项目基本都是使用框架进行开发,今天我就介绍如何使用 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。
项目搭建完成之后,我们把文件整理下,删除一些我们不用的文件,同时把相关引用也删除,最终文件目录如下:
当我们使用 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),进入到源代码中去,然后一直去查找我们的方法的定义,首先我们进入到了:
然后 InternalForm 继承了 InternalForm,我们再继续去寻找,最后找到了源头:
同理我们也可以找到 onFinishFailed:
最后在文件中引入这两个类型即可。
经过上面的测试之后,我们的项目基本上就算已经搭建好了,接下来就可以继续充实相关的页面了。
这里再把文件整理下,把不需要的删除,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的知识点”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。