23 前面跳过的unkown类型详解

学习完交叉类型、联合类型、类型断言、映射类型、索引后,我们就可以补充一个基础类型中没有讲的知识了,就是 TS 在 3.0 版本新增的顶级类型 unknown。它相对于 any 来说是安全的。关于 unknown 类型,有如下几点需要注意,我们来逐个讲解和举例学习: (1) 任何类型的值都可以赋值给 unknown 类型: let value1: unknown; value1 = "a"; value1 = 123; (2) 如果没有类型断言或基于控制流的类型细化时 unknown 不可以赋值给其它类型,此时它只能赋值给 unknown 和 any 类型: let value2: unknown; let value3: string = value2; // error 不能将类型“unknown”分配给类型“string” value1 = value2; (3) 如果没有类型断言或基于控制流的类型细化,则不能在它上面进行任何操作: let value4: unknown; value4 += 1; // error 对象的类型为 "unknown" (4) unknown 与任何其它类型组成的交叉类型,最后都等于其它类型: type type1 = unknown & string; // type1 => string type type2 = number & unknown; // type2 => number type type3 = unknown & unknown; // type3 => unknown type type4 = unknown & string[]; // type4 => string[] (5) unknown 与任何其它类型组成的联合类型,都等于 unknown 类型,但只有any例外,unknown与any组成的联合类型等于any):...

March 13, 2024 · zlzong

24 条件类型,它不是三元操作符的写法吗?

3.11.1 基础使用 条件类型是 TS2.8 引入的,从语法上看它像是三元操作符。它会以一个条件表达式进行类型关系检测,然后在后面两种类型中选择一个,先来看它怎么写: T extends U ? X : Y 这个表达式的意思是,如果 T 可以赋值给 U 类型,则是 X 类型,否则是 Y 类型。来看个实际例子: type Type<T> = T extends string | number let index: Type<'a'> // index的类型为string let index2: Type<false> // index2的类型为number 3.11.2 分布式条件类型 当待检测的类型是联合类型,则该条件类型被称为“分布式条件类型”,在实例化时会自动分发成联合类型,来看例子: type TypeName<T> = T extends any ? T : never; type Type1 = TypeName<string | number>; // Type1的类型是string|number 你可能会说,既然想指定 Type1 的类型为 string|number,为什么不直接指定,而要使用条件类型?其实这只是简单的示范,条件类型可以增加灵活性,再来看个复杂点的例子,这是官方文档的例子: type TypeName<T> = T extends string ?...

March 13, 2024 · zlzong

25 入手装饰器,给凡人添加超能力

ECMAScript 的装饰器提案到现在还没有定案,所以我们直接看 TS 中的装饰器。同样在 TS 中,装饰器仍然是一项实验性特性,未来可能有所改变,所以如果你要使用装饰器,需要在 tsconfig.json 的编译配置中开启experimentalDecorators,将它设为 true。 3.12.1. 基础 (1) 装饰器定义 装饰器是一种新的声明,它能够作用于类声明、方法、访问符、属性和参数上。使用@符号加一个名字来定义,如@decorat,这的 decorat 必须是一个函数或者求值后是一个函数,这个 decorat 命名不是写死的,是你自己定义的,这个函数在运行的时候被调用,被装饰的声明作为参数会自动传入。要注意装饰器要紧挨着要修饰的内容的前面,而且所有的装饰器不能用在声明文件(.d.ts)中,和任何外部上下文中(比如 declare,关于.d.ts 和 declare,我们都会在讲声明文件一课时学习)。比如下面的这个函数,就可以作为装饰器使用: function setProp (target) { // ... } @setProp 先定义一个函数,然后这个函数有一个参数,就是要装饰的目标,装饰的作用不同,这个target代表的东西也不同,下面我们具体讲的时候会讲。定义了这个函数之后,它就可以作为装饰器,使用@函数名的形式,写在要装饰的内容前面。 (2) 装饰器工厂 装饰器工厂也是一个函数,它的返回值是一个函数,返回的函数作为装饰器的调用函数。如果使用装饰器工厂,那么在使用的时候,就要加上函数调用,如下: function setProp () { return function (target) { // ... } } @setProp() (3) 装饰器组合 装饰器可以组合,也就是对于同一个目标,引用多个装饰器: // 可以写在一行 @setName @setAge target // 可以换行 @setName @setAge target 但是这里要格外注意的是,多个装饰器的执行顺序: 装饰器工厂从上到下依次执行,但是只是用于返回函数但不调用函数; 装饰器函数从下到上依次执行,也就是执行工厂函数返回的函数。 我们以下面的两个装饰器工厂为例: function setName () { console.log('get setName') return function (target) { console....

March 13, 2024 · zlzong

26 使用模块封装代码

TypeScript 在 1.5 版本之前,有内部模块和外部模块的概念,从 1.5 版本开始,内部模块改称作命名空间(我们下个小节会讲),外部模块改称为模块。如果你对模块的知识一无所知,建议你先重点学习一下 CommonJS 模块系统和 ES6模块系统,TypeScript 中的模块系统是遵循 ES6 标准的,所以你需要重点学习 ES6 标准中的模块知识,这里推荐大家几个链接,大家可以在这里去学习一下: CommonJS/AMD/CMD/ES6规范 ECMAScript6入门 - Module 的语法 TypeScript 和 ES6 保持一致,包含顶级 import 或 export 的文件都被当成一个模块,则里面定义的内容仅模块内可见,而不是全局可见。TypeScript 的模块除了遵循 ES6 标准的模块语法外,还有一些特定语法,用于类型系统兼容多个模块格式,下面我们来开始学习 TypeScript 模块。 4.1.1. export TypeScript 中,仍然使用 export 来导出声明,而且能够导出的不仅有变量、函数、类,还包括 TypeScript 特有的类型别名和接口。 // funcInterface.ts export interface Func { (arg: number): string; } export class C { constructor() {} } class B {} export { B }; export { B as ClassB }; 上面例子中,你可以使用 export 直接导出一个声明,也可以先声明一个类或者其它内容,然后使用 export {}的形式导出,也可以使用 as 来为导出的接口换个名字再导出一次。...

March 13, 2024 · zlzong

27 使用命名空间封装代码

命名空间在 1.5 之前的版本中,是叫做“内部模块”。在 1.5 版本之前,ES6 模块还没正式成为标准,所以 TS 对于模块的实现,是将模块分为“内部模块”和“外部模块”两种。内部模块使用module来定义,而外部模块使用export来指定哪个内容对外部可见。 1.5 版本开始,使用“命名空间”代替“内部模块”说法,并且使用 namespace 代替原有的 module关键字,而“外部 模块”则改为“模块”。 命名空间的作用与使用场景和模块还是有区别的: 当我们是在程序内部用于防止全局污染,想把相关的内容都放在一起的时候,使用命名空间; 当我们封装了一个工具或者库,要适用于模块系统中引入使用时,适合使用模块。 4.2.1 定义和使用 命名空间的定义实际相当于定义了一个大的对象,里面可以定义变量、接口、类、方法等等,但是如果不使用export 关键字指定此内容要对外可见的话,外部是没法访问到的。来看下怎么写,我们想要把所有涉及到内容验证的方法都放到一起,文件名叫 validation.ts: namespace Validation { const isLetterReg = /^[A-Za-z]+$/; // 这里定义一个正则 export const isNumberReg = /^[0-9]+$/; // 这里再定义一个正则,与isLetterReg的区别在于他使用export导出了 export const checkLetter = (text: any) => { return isLetterReg.test(text); }; } 我们创建了一个命名空间叫做 Validation,它里面定义了三个内容,两个正则表达式,但是区别在于 isLetterReg 没有使用 export 修饰,而 isNumberReg 使用了 export 修饰。最后一个函数,也是用了 export 修饰。 这里要说明一点的是,命名空间在引入的时候,如果是使用 tsc 命令行编译文件,比如是在index.ts文件使用这个命名空间,先直接像下面这样写: /// <reference path="validation.ts"/> let isLetter = Validation....

March 13, 2024 · zlzong

28 对声明合并的爱与恨

声明合并是指 TypeScript 编译器会将名字相同的多个声明合并为一个声明,合并后的声明同时拥有多个声明的特性。我们知道在 JavaScrip 中,使用var关键字定义变量时,定义相同名字的变量,后面的会覆盖前面的值。使用let 定义变量和使用 const 定义常量时,不允许名字重复。在 TypeScript 中,接口、命名空间是可以多次声明的,最后 TypeScript 会将多个同名声明合并为一个。我们下来看个简单的例子: interface Info { name: string } interface Info { age: number } let info: Info info = { // error 类型“{ name: string; }”中缺少属性“age” name: 'lison' } info = { // right name: 'lison', age: 18 } 可以看到,我们定义了两个同名接口Info,每个接口里都定义了一个必备属性,最后定义info类型为Info时,info的定义要求同时包含name和age属性。这就是声明合并的简单示例,接下来我们详细学习。 4.3.1. 补充知识 TypeScript的所有声明概括起来,会创建这三种实体之一:命名空间、类型和值: 命名空间的创建实际是创建一个对象,对象的属性是在命名空间里export导出的内容; 类型的声明是创建一个类型并赋给一个名字; 值的声明就是创建一个在JavaScript中可以使用的值。 下面这个表格会清晰的告诉你,每一种声明类型会创建这三种实体中的哪种,先来说明一下,第一列是指声明的内容,每一行包含4列,表明这一行中,第一列的声明类型创建了后面三列哪种实体,打钩即表示创建了该实体: 声明类型 创建了命名空间 创建了类型 创建了值 Namespace √ √ Class √ √ Enum √ √ Interface √ Type Alias类型别名 √ Function √ Variable √ 可以看到,只要命名空间创建了命名空间这种实体。Class、Enum两个,Class即是实际的值也作为类使用,Enum编译为JavaScript后也是实际值,而且我们讲过,一定条件下,它的成员可以作为类型使用;Interface和类型别名是纯粹的类型;而Funciton和Variable只是创建了JavaScript中可用的值,不能作为类型使用,注意这里Variable是变量,不是常量,常量是可以作为类型使用的。...

March 13, 2024 · zlzong

29 混入,兼顾值和类型的合并操作

混入即把两个对象或者类的内容,混合起来,从而实现一些功能的复用。如果你使用过 Vue,你应该知道 Vue 的 mixins 这个 api,它可以允许你将一些抽离到对象的属性、方法混入到一些组件。接下来我们先来看看个在 JavaScript 中实现的简单混入: class A { constructor() {} funcA() { console.log("here"); } } class B { constructor() {} funcB() {} } const mixin = (target, from) => { // 这里定义一个函数来将一个类混入到目标类 Object.getOwnPropertyNames(from).forEach(key => { // 通过Object.getOwnPropertyNames可以获取一个对象自身定义的而非继承来的属性名组成的数组 target[key] = from[key]; // 将源类原型对象上的属性拿来添加到目标类的原型对象上 }); }; mixin(B.prototype, A.prototype); // 传入两个类的原型对象 const b = new B(); b.funcA(); // here 我们通过Object.getOwnPropertyNames方法获取一个对象自身的属性,这里自身指除去继承的属性,获取到属性后将属性赋值给目标对象。 这是 JavaScript 中的简单混入,在 TypeScript 中我们知道,除了值还有类型的概念,所以简单地将属性赋值到目标元素是不行的,还要处理类型定义,我们来看下 TypeScript 中混入的例子: class ClassAa { isA: boolean; funcA() {} } class ClassBb { isB: boolean; funcB() {} } // 定义一个类类型接口AB,我们在讲类的时候补充过类和接口之间的继承,也讲过类类型接口 // 这里是让类AB继承ClassAa和ClassBb的类型,所以使用implements关键字,而不是用extends class AB implements ClassAa, ClassBb { constructor() {} isA: boolean = false; // 定义两个实例属性 isB: boolean = false; funcA: () => void; // 定义两个方法,并指定类型 funcB: () => void; } function mixins(base: any, from: any[]) { // 这里我们改造一下,直接传入类,而非其原型对象,base是我们最后要汇总而成的类,from是个数组,是我们要混入的源类组成的数组 from....

March 13, 2024 · zlzong

30 Promise及其语法糖async和await

TS 在 1.6 版本实验性地支持了 async 函数。在过去的 JavaScript 当中,如果想保证代码的执行顺序,需要使用回调函数,当需要执行的步骤多了时就会陷入当说的“回调地狱”。自从 ES6 增加了 Promise 之后,状况有了缓解,我们先来看个例子,一个简单的多个 ajax 请求的例子: ajax.post( // 这里你可以先忽略ajax的定义,他的post方法用来发送一个post请求 "/login", // 第一个参数时要请求的url { data: { user_name: "lison", password: "xxxxx" } }, // 第二个参数是这个请求要携带的参数 function(res) { var user_id = res.data.user_id; ajax.post( // 这里在/login接口成功返回数据后,再调用一个/user_roles接口,用来获取该登录用户的角色信息 "/user_roles", { data: { user_id: user_id } }, function(res) { var role = res.data.role; console.log(role); } ); } // 第三个参数是接口响应之后的回调函数 ); 在这个例子中,我们先调用登录的接口发送用户名和密码,然后服务端进行校验之后返回这个用户的一些信息,然后我们可以从信息中拿到用户 id 去获取它的角色用于权限控制。这个过程是有先后顺序的,必须先登录后获取角色,为了保证这个顺序,在过去要使用回调函数,当然一些库也支持链式调用。再来看下使用 ES6 的 Promise 需要怎么写: const loginReq = ({ user_name, password }) => { // 封装一个loginReq函数,用来返回一个Promise,用来调用/login接口 return new Promise((resolve, reject) => { // Promise接收一个回调函数参数,这个函数有两个参数,两个参数都是回调函数 ajax....

March 13, 2024 · zlzong

31 逐条来看tsconfig.json配置

本小结我们主要讲 tsconfig.json 文件的可配项以及功能。 tsconfig.json 是放在项目根目录,用来配置一些编译选项等。当我们使用 tsc 命令编译项目,且没有指定输入文件时,编译器就会去查找 tsconfig.json 文件。如果在当前目录没找到,就会逐级向父文件夹查找。我们也可以通过在 tsc 命令中加上–project 参数,来指定一个包含 tsconfig.json 文件的目录。如果命令行上指定了输入文件时,tsconfig.json 的配置会被忽略。 # 直接在项目根目录下执行tsc命令,会自动根据tsconfig.json配置项编译 tsc # 指定要编译的项目,即tsconfig.json所在文件目录 tsc --project ./dir/project # 指定要编译的文件,忽略tsconfig.json文件配置 tsc ./src/main.ts 接下来我们看一下 tsconfig.json 里都有哪些可配置项。tsconfig.json 文件里有几个主要的配置项: { "compileOnSave": true, "files": [], "include": [], "exclude": [], "extends": "", "compilerOptions": {} } 我们来逐个学习它们的作用,以及可配置的值: [1] compileOnSave compileOnSave 的值是 true 或 false。如果设为 true,在我们编辑了项目中文件保存的时候,编辑器会根据 tsconfig.json 的配置重新生成文件,不过这个要编辑器支持。 [2] files files 可以配置一个数组列表,里面包含指定文件的相对或绝对路径。编译器在编译的时候只会编译包含在 files 中列出的文件。如果不指定,则取决于有没有设置 include 选项;如果没有 include 选项,则默认会编译根目录以及所有子目录中的文件。这里列出的路径必须是指定文件,而不是某个文件夹,而且不能使用*、?、**/等通配符。 [3] include include 也可以指定要编译的路径列表,但和 files 的区别在于,这里的路径可以是文件夹,也可以是文件,可以使用相对和绝对路径,而且可以使用通配符。比如"....

March 13, 2024 · zlzong

32 书写声明文件之磨刀:识别库类型

前面我们提到几次.d.ts后缀的文件,这节课我们来完整学习下与声明文件相关的内容。 我们之前讲模块的时候讲到过两种常见模块标准,即 CommonJS 和 RequireJS。不同的模块在实现方式上是不一样的。我们要为已有的第三方 JS 库编写声明文件,以便在 TS 中更好地使用类型系统,所以首先需要知道我们使用的 JS 库被编译成了什么类型。我们来分别看下几种类型的特征: 5.2.1. 全局库 在一开始,没有 webpack 这些编译工具的时候,我们都是在 html 文件里使用 script 标签引入 js 文件,然后就可以在引入的后面使用引入的库了。比如我们使用 jQuery,只需要在<body>标签里通过<script src=“http://xxx.com/jQuery.min.js”></script>引入 jQuery 库,然后就可以在<script></script>标签内使用: $(function() { // ... }); 这种不需要我们引入什么变量,只需要将库引入即可使用的库,就叫做全局库。后面讲到 UMD 模块的时候要注意,UMD 模块既可以作为模块使用,又可以作为全局库使用的模块,所以在判断一个库的时候,如果它可以像例子中那样全局使用,首先要确定它是不是 UMD 模块;如果不是,那它可能就是一个单纯的全局库。 另外,你还可以通过看库的源码,来判断它是什么类型,一个全局库,通常会包含下面内容中的一个或多个: 顶级的 var 语句或 function 声明; 一个或多个赋值给 window.someName 的赋值语句; 判断 document 或 window 是否存在的判断逻辑。 因为顶级的 var 或 function 是直接在全局环境声明变量或函数,不使用立即执行函数包裹会影响到全局,所以有这种一般会是全局库;当出现给 window 设置某个属性名 someName ,然后给这个属性赋值的语句时,是在给全局对象 window 赋值。引入这个库后直接通过 window.someName 即可在全局任何地方访问到这个属性值;如果出现 if 语句或三元操作符这种判断 document 或 window 是否存在的语句,也有可能是要给这两个全局对象添加内容,所以也有可能是全局库。...

March 13, 2024 · zlzong