21 索引类型:获取索引类型和索引值类型

我们这里要讲的,可不是前面讲接口的时候讲的索引类型。在学习接口内容的时候,我们讲过可以指定索引的类型。而本小节我们讲的索引类型包含两个内容:索引类型查询和索引访问操作符。 3.8.1 索引类型查询操作符 keyof操作符,连接一个类型,会返回一个由这个类型的所有属性名组成的联合类型。来看例子: interface Info { name: string; age: number; } let infoProp: keyof Info; infoProp = "name"; infoProp = "age"; infoProp = "no"; // error 不能将类型“"no"”分配给类型“"name" | "age"” 通过例子可以看到,这里的keyof Info其实相当于"name" | “age”。通过和泛型结合使用,TS 就可以检查使用了动态属性名的代码: function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] { // 这里使用泛型,并且约束泛型变量K的类型是"keyof T",也就是类型T的所有字段名组成的联合类型 return names.map(n => obj[n]); // 指定getValue的返回值类型为T[K][],即类型为T的值的属性值组成的数组 } const info = { name: "lison", age: 18 }; let values: string[] = getValue(info, ["name"]); values = getValue(info, ["age"]); // error 不能将类型“number[]”分配给类型“string[]” 3....

March 13, 2024 · zlzong

22 使用映射类型得到新的类型

3.9.1 基础 TS 提供了借助旧类型创建一个新类型的方式,也就是映射类型,它可以用相同的形式去转换旧类型中每个属性。来看个例子: interface Info { age: number; } 我们可以使用这个接口实现一个有且仅有一个 age 属性的对象,但如果我们想再创建一个只读版本的同款对象,那我们可能需要再重新定义一个接口,然后让 age 属性 readonly。如果接口就这么简单,你确实可以这么做,但是如果属性多了,而且这个结构以后会变,那就比较麻烦了。这种情况我们可以使用映射类型,下面来看例子: interface Info { age: number; } type ReadonlyType<T> = { readonly [P in keyof T]: T[P] }; // 这里定义了一个ReadonlyType<T>映射类型 type ReadonlyInfo = ReadonlyType<Info>; let info: ReadonlyInfo = { age: 18 }; info.age = 28; // error Cannot assign to 'age' because it is a constant or a read-only property 这个例子展示了如何通过一个普通的接口创建一个每个属性都只读的接口,这个过程有点像定义了一个函数,这个函数会遍历传入对象的每个属性并做处理。同理你也可以创建一个每个属性都是可选属性的接口: interface Info { age: number; } type ReadonlyType<T> = { readonly [P in keyof T]?...

March 13, 2024 · zlzong

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