当前位置: 首页 > news >正文

做网站需要哪些程序员网站关键词整体方案

做网站需要哪些程序员,网站关键词整体方案,seo外包公司怎么样,泉州seo建站TS 基础 Typescript 在线编译平台 基础类型 boolean、number 和 string 类型 boolean let isHandsome: boolean true赋值与定义的不一致#xff0c;会报错#xff0c;静态类型语言的优势就体现出来了#xff0c;可以帮助我们提前发现代码中的错误。 let isHandsome: …TS 基础 Typescript 在线编译平台 基础类型 boolean、number 和 string 类型 boolean let isHandsome: boolean true赋值与定义的不一致会报错静态类型语言的优势就体现出来了可以帮助我们提前发现代码中的错误。 let isHandsome: boolean true // Type string is not assignable to type boolean.number let age: number 18string let realName: string li let fullName: string A ${realName} // 支持模板字符串undefined 和 null 类型 let u: undefined undefined // undefined 类型 let n: null null // null 类型默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。 let age: number null let realName: string undefined但是如果指定了 --strictNullChecks 标记null 和 undefined 只能赋值给 void 和它们各自不然会报错。 let age: number null // Type null is not assignable to type number.any、unknown 和 void 类型 any 不清楚用什么类型可以使用 any 类型。这些值可能来自于动态的内容比如来自用户输入或第三方代码库 let notSure: any 4 notSure maybe a string // 可以是 string 类型 notSure false // 也可以是 boolean 类型notSure.name // 可以随便调用属性和方法 notSure.getName()不建议使用 any不然就丧失了 TS 的意义。 unknown 类型 不建议使用 any当我不知道一个类型具体是什么时该怎么办 可以使用 unknown 类型 unknown 类型代表任何类型它的定义和 any 定义很像但是它是一个安全类型使用 unknown 做任何事情都是不合法的。 比如这样一个 divide 函数 function divide(param: any) {return param / 2 }把 param 定义为 any 类型TS 就能编译通过没有把潜在的风险暴露出来万一传的不是 number 类型不就没有达到预期了吗。 把 param 定义为 unknown 类型 TS 编译器就能拦住潜在风险 function divide(param: unknown) {return param / 2 // param is of type unknown. }因为不知道 param 的类型使用运算符 /导致报错。 再配合类型断言即可解决这个问题 function divide(param: unknown) {return (param as number) / 2 }void void类型与 any 类型相反它表示没有任何类型。 比如函数没有明确返回值默认返回 Void 类型 function welcome(): void {console.log(hello) }never 类型 never类型表示的是那些永不存在的值的类型。 有些情况下值会永不存在比如 如果一个函数执行时抛出了异常那么这个函数永远不存在返回值因为抛出异常会直接中断程序运行。函数中执行无限循环的代码使得程序永远无法运行到函数返回值那一步。 // 异常 function fn(msg: string): never {throw new Error(msg) }// 死循环 千万别这么写会内存溢出 function fn(): never {while (true) {} }never 类型是任何类型的子类型也可以赋值给任何类型。 没有类型是 never 的子类型没有类型可以赋值给 never 类型除了 never 本身之外。即使 any也不可以赋值给 never 。 let test1: never test1 li // 报错Type string is not assignable to type neverlet test1: never let test2: anytest1 test2 // 报错Type any is not assignable to type never数组类型 let list: number[] [1, 2, 3] list.push(4) // 可以调用数组上的方法数组里的项写错类型会报错 let list: number[] [1, 2, 3] // Type string is not assignable to type numberpush 时类型对不上会报错 let list: number[] [1, 2, 3] list.push(str) // Argument of type string is not assignable to parameter of type number.如果数组想每一项放入不同数据怎么办用元组类型 元组类型 元组类型允许表示一个已知元素数量和类型的数组各元素的类型不必相同。 let tuple: [number, string] [18, li]写错类型会报错 let tuple: [number, string] [18, li] // Type string is not assignable to type number.越界会报错 let tuple: [number, string] [18, li, 100] // Type [number, string, number] is not assignable to type [number, string]. Source has 3 element(s) but target allows only 2.可以对元组使用数组的方法比如使用 push 时不会有越界报错 let tuple: [number, string] [18, li] tuple.push(100) // 但是只能 push 定义的 number 或者 string 类型push 一个没有定义的类型报错 let tuple: [number, string] [18, li] tuple.push(true) // Argument of type boolean is not assignable to parameter of type string | number.函数类型 TS 定义函数类型需要定义输入参数类型和输出类型。 输出类型也可以忽略因为 TS 能够根据返回语句自动推断出返回值类型。 function add(x: number, y: number): number {return x y } add(1, 2)函数没有明确返回值默认返回 Void 类型 function welcome(): void {console.log(hello); }函数表达式写法 let add2 (x: number, y: number): number {return x y }可选参数 参数后加个问号代表这个参数是可选的 function add(x:number, y:number, z?:number):number {return x y }add(1,2,3) add(1,2)注意可选参数要放在函数入参的最后面不然会导致编译错误。 function add(x: number, y?: number, z: number): number {return x y } // z is declared but its value is never read.(6133) // A required parameter cannot follow an optional parameter.默认参数 function add(x: number, y: number 100): number {return x y }add(100) // 200跟 JS 的写法一样在入参里定义初始值。 和可选参数不同的是默认参数可以不放在函数入参的最后面 function add(x: number 100, y: number): number {return x y }add(100) // Expected 2 arguments, but got 1.(2554) // input.tsx(1, 30): An argument for y was not provided.看上面的代码add 函数只传了一个参数如果理所当然地觉得 x 有默认值只传一个就传的是 y 的话就会报错。 编译器会判定你只传了 x没传 y。 如果带默认值的参数不是最后一个参数用户必须明确的传入 undefined值来获得默认值。 add(undefined, 100) // 200函数赋值 JS 中变量随便赋值没问题 let fn () {} fn 123但在 TS 中函数不能随便赋值会报错的。 也可以用下面这种方式定义一个函数 add3把 add2 赋值给 add3 let add2 (x: number, y: number): number {return x y }const add3: (x: number, y: number) number add2有点像 es6 中的箭头函数但不是箭头函数TS 遇到 : 就知道后面的代码是写类型用的。 当然不用定义 add3 类型直接赋值也可以TS 会在变量赋值的过程中自动推断类型如下 let add2 (x: number, y: number): number {return x y }const add3 add2 // const add3: (x: number, y: number) number函数重载 函数重载是指两个函数名称相同但是参数个数或参数类型不同他的好处显而易见不需要把相似功能的函数拆分成多个函数名称不同的函数。 不同参数类型 比如我们实现一个 add 函数如果传入参数都是数字就返回数字相加如果传入参数都是字符串就返回字符串拼接 function add(x: number[]): number function add(x: string[]): string function add(x: any[]): any {if (typeof x[0] string) {return x.join()}if (typeof x[0] number) {return x.reduce((acc, cur) acc cur)} }在 TS 中实现函数重载需要多次声明这个函数前几次是函数定义列出所有的情况最后一次是函数实现需要比较宽泛的类型比如上面的例子就用到了 any。 不同参数个数 假设这个 add 函数接受更多的参数个数比如还可以传入一个参数 y如果传了 y就把 y 也加上或拼接上就可以这么写 function add(x: number[]): number function add(x: string[]): string function add(x: number[], y: number[]): number function add(x: string[], y: string[]): string function add(x: any[], y?: any[]): any {if (Array.isArray(y) typeof y[0] number) {return x.reduce((acc, cur) acc cur) y.reduce((acc, cur) acc cur)}if (Array.isArray(y) typeof y[0] string) {return x.join() , y.join()}if (typeof x[0] string) {return x.join()}if (typeof x[0] number) {return x.reduce((acc, cur) acc cur)} }console.log(add([1, 2, 3])) // 6 console.log(add([li, 18])) // lin,18 console.log(add([1, 2, 3], [1, 2, 3])) // 12 console.log(add([li, 18], [man, handsome])) // li,18,man,handsome其实写起来挺麻烦的后面了解泛型之后写起来会简洁一些不必太纠结函数重载知道有这个概念即可平时一般用泛型来解决类似问题。 interface 基本概念 interface(接口) 是 TS 设计出来用于定义对象类型的可以对对象的形状进行描述。 定义 interface 一般首字母大写代码如下 interface Person {name: stringage: number }const p1: Person {name: li,age: 18, }属性必须和类型定义的时候完全一致。 少写了属性报错 interface Person {name: stringage: number }const p1: Person {name: li, } // Property age is missing in type { name: string; } but required in type Person.多写了属性报错 interface Person {name: stringage: number }const p1: Person {name: li,age: 18,work: null, } // Object literal may only specify known properties, and work does not exist in type Person.注意interface 不是 JS 中的关键字所以 TS 编译成 JS 之后这些 interface 是不会被转换过去的都会被删除掉interface 只是在 TS 中用来做静态检查。 可选属性 跟函数的可选参数是类似的在属性上加个 ?这个属性就是可选的比如下面的 age 属性 interface Person {name: stringage?: number }const p1: Person {name: li, }只读属性 如果希望某个属性不被改变可以这么写 interface Person {readonly id: numbername: stringage: number }改变这个只读属性时会报错。 interface Person {readonly id: numbername: stringage: number }const p1: Person {id: 1,name: li,age: 18, } p1.id 2 // Cannot assign to id because it is a read-only property.interface 描述函数类型 interface 也可以用来描述函数类型代码如下 interface ISum {(x: number, y: number): number }const add: ISum (num1, num2) {return num1 num2 }自定义属性可索引的类型 上文中属性必须和类型定义的时候完全一致如果一个对象上有多个不确定的属性怎么办 可以这么写。 interface RandomKey {[propName: string]: string }const obj: RandomKey {a: hello,b: lin,c: welcome, }如果把属性名定义为 number 类型就是一个类数组了看上去和数组一模一样。 interface LikeArray {[propName: number]: string }const arr: LikeArray [hello, li]arr[0] // 可以使用下标来访问值当然不是真的数组数组上的方法它是没有的。 interface LikeArray {[propName: number]: string } const arr: LikeArray [hello, li] arr.push(18) // Property push does not exist on type LikeArray.duck typing 看到这里你会发现interface 的写法非常灵活它不是教条主义。 用 interface 可以创造一系列自定义的类型。 事实上 interface 还有一个响亮的名称duck typing(鸭子类型)。 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子那么这只鸟就可以被称为鸭子。 – James Whitcomb Riley 这句话完美地诠释了 interface 的含义只要数据满足了 interface 定义的类型TS 就可以编译通过。 举个例子 interface FunctionWithProps {(x: number): numbername: string }FunctionWithProps 接口描述了一个函数类型还向这个函数类型添加了 name 属性这看上去完全是四不像但是这个定义是完全可以工作的。 const fn: FunctionWithProps (x) {return x }fn.name hello world类 我们知道 JS 是靠原型和原型链来实现面向对象编程的es6 新增了语法糖 class。 TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。 在 TS 中写法和 JS 差不多只是要定义一些类型而已我们通过下面几个例子来复习一下类的封装、继承和多态。 基本写法 定义一个 Person 类有属性 name 和 方法 speak class Person {name: stringconstructor(name: string) {this.name name}speak() {console.log(${this.name} is speaking)} }const p1 new Person(li) // 新建实例p1.name // 访问属性和方法 p1.speak()继承 使用 extends 关键字实现继承定义一个 Student 类继承自 Person 类。 class Student extends Person {study() {console.log(${this.name} needs study)} }const s1 new Student(li)s1.study()继承之后Student 类上的实例可以访问 Person 类上的属性和方法。 super 关键字 注意上例中 Student 类没有定义自己的属性可以不写 super 但是如果 Student 类有自己的属性就要用到 super 关键字来把父类的属性继承过来。 比如Student 类新增一个 grade(成绩) 属性就要这么写 class Student extends Person {grade: numberconstructor(name: string, grade: number) {super(name)this.grade grade} }const s1 new Student(li, 100)不写 super 会报错。 class Student extends Person {grade: numberconstructor(grade: number) {// Constructors for derived classes must contain a super call.this.grade grade} }const s1 new Student(li, 100)多态 子类对父类的方法进行了重写子类和父类调同一个方法时会不一样。 class Student extends Person {speak() {return Student ${super.speak()}} }TS 中一般对抽象方法实现多态详细见后文抽象类。 public public公有的一个类里默认所有的方法和属性都是 public。 比如上文中定义的 Person 类其实是这样的 class Person {public name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(${this.name} is speaking)} }public 可写可不写不写默认也是 public。 private private私有的只属于这个类自己它的实例和继承它的子类都访问不到。 将 Person 类的 name 属性改为 private。 class Person {private name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(${this.name} is speaking)} }实例访问 name 属性会报错 class Person {private name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(${this.name} is speaking)} }const p1 new Person(li) p1.name // Property name is private and only accessible within class Person.继承它的子类 访问 name 属性会报错 class Student extends Person {study() {console.log(${this.name} needs study) // Property name is private and only accessible within class Person.} }protected protected 受保护的继承它的子类可以访问实例不能访问。 将 Person 类的 name 属性改为 protected。 class Person {protected name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(${this.name} is speaking)} }实例访问 name 属性会报错 class Person {protected name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(${this.name} is speaking)} }const p1 new Person(li) p1.name // Property name is protected and only accessible within class Person and its subclasses.子类可以访问。 class Studeng extends Person {study() {console.log(${this.name} needs study)} }static static 是静态属性可以理解为是类上的一些常量实例和子类都不能访问。 比如一个 Circle 类圆周率是 3.14可以直接定义一个静态属性。 class Circle {static pi: 3.14public radius: numberpublic constructor(radius: number) {this.radius radius}public calcLength() {return Circle.pi * this.radius * 2 // 计算周长直接访问 Circle.pi} }实例访问会报错 class Circle {static pi: 3.14public radius: numberpublic constructor(radius: number) {this.radius radius}public calcLength() {return Circle.pi * this.radius * 2 // 计算周长直接访问 Circle.pi} }const c1 new Circle(10) c1.pi // Property pi does not exist on type Circle. Did you mean to access the static member Circle.pi instead?抽象类 抽象类听名字似乎是非常难理解的概念但其实非常简单。 TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。 其实 TS 还对 JS 扩展了一个新概念——抽象类。 所谓抽象类是指只能被继承但不能被实例化的类就这么简单。 抽象类有两个特点 抽象类不允许被实例化抽象类中的抽象方法必须被子类实现 抽象类用一个 abstract 关键字来定义我们通过两个例子来感受一下抽象类的两个特点。 抽象类不允许被实例化 abstract class Animal {}const a new Animal() // Cannot create an instance of an abstract class.定义一个抽象类 Animal初始化一个 Animal 的实例直接报错。 抽象类中的抽象方法必须被子类实现 abstract class Animal {constructor(name: string) {this.name name}public name: stringpublic abstract sayHi(): void }class Dog extends Animal {constructor(name: string) {super(name)} } // Non-abstract class Dog does not implement all abstract members of Animal(18052) // input.tsx(6, 21): Non-abstract class Dog does not implement inherited abstract member sayHi from class Animal.定义一个 Dog 类继承自 Animal 类但是却没有实现 Animal 类上的抽象方法 sayHi报错。 正确的用法如下 abstract class Animal {constructor(name: string) {this.name name}public name: stringpublic abstract sayHi(): void }class Dog extends Animal {constructor(name: string) {super(name)}public sayHi() {console.log(wang)} }为什么叫抽象类 很显然抽象类是一个广泛和抽象的概念不是一个实体就比如上文的例子动物这个概念是很广泛的猫、狗、狮子都是动物但动物却不好是一个实例实例只能是猫、狗或者狮子。 官方一点的说法是在面向对象的概念中所有的对象都是通过类来描绘的但是反过来并不是所有的类都是用来描绘对象的如果一个类中没有包含足够的信息来描绘一个具体的对象这样的类就是抽象类。 比如 Animal 类只是具有动物都有的一些属性和方法但不会具体到包含猫或者狗的属性和方法。 所以抽象类的用法是用来定义一个基类声明共有属性和方法拿去被继承。 抽象类的好处是可以抽离出事物的共性有利于代码的复用。 抽象方法和多态 多态是面向对象的三大基本特征之一。 多态指的是父类定义一个抽象方法在多个子类中有不同的实现运行的时候不同的子类就对应不同的操作比如 abstract class Animal {constructor(name: string) {this.name name}public name: stringpublic abstract sayHi(): void }class Dog extends Animal {constructor(name: string) {super(name)}public sayHi() {console.log(wang)} }class Cat extends Animal {constructor(name: string) {super(name)}public sayHi() {console.log(miao)} }Dog 类和 Cat 类都继承自 Animal 类Dog 类和 Cat 类都不同的实现了 sayHi 这个方法。 this 类型 接下来我们介绍一种特殊的类型this 类型。 类的成员方法可以直接返回一个 this这样就可以很方便地实现链式调用。 链式调用 class StudyStep {step1() {console.log(listen)return this}step2() {console.log(write)return this} }const s new StudyStep()s.step1().step2() // 链式调用灵活调用子类父类方法 在继承的时候this 可以表示父类型也可以表示子类型 class StudyStep {step1() {console.log(listen)return this}step2() {console.log(write)return this} }class MyStudyStep extends StudyStep {next() {console.log(before done, study next!)return this} }const m new MyStudyStep()m.step1().next().step2().next() // 父类型和子类型上的方法都可随意调用这样就保持了父类和子类之间接口调用的连贯性 interface 和 class 的关系 上文中我们说过interface 是 TS 设计出来用于定义对象类型的可以对对象的形状进行描述。 interface 同样可以用来约束 class要实现约束需要用到 implements 关键字。 implements implements 是实现的意思class 实现 interface。 比如手机有播放音乐的功能可以这么写 interface MusicInterface {playMusic(): void }class Cellphone implements MusicInterface {playMusic() {} }定义了约束后class 必须要满足接口上的所有条件。 如果 Cellphone 类上不写 playMusic 方法会报错。 interface MusicInterface {playMusic(): void }class Cellphone implements MusicInterface { } // Class Cellphone incorrectly implements interface MusicInterface.Property playMusic is missing in type Cellphone but required in type MusicInterface.处理公共的属性和方法 不同的类有一些共同的属性和方法使用继承很难完成。 比如汽车Car 类也有播放音乐的功能你可以这么做 用 Car 类继承 Cellphone 类找一个 Car 类和 Cellphone 类的父类父类有播放音乐的方法他们俩继承这个父类 很显然这两种方法都不合常理。 实际上使用 implements问题就会迎刃而解。 interface MusicInterface {playMusic(): void }class Car implements MusicInterface {playMusic() {} }class Cellphone implements MusicInterface {playMusic() {} }这样 Car 类和 Cellphone 类都约束了播放音乐的功能。 再比如手机还有打电话的功能就可以这么做Cellphone 类 implements 两个 interface。 interface MusicInterface {playMusic(): void }interface CallInterface {makePhoneCall(): void }class Cellphone implements MusicInterface, CallInterface {playMusic() {}makePhoneCall() {} }这个 CallInterface 也可以用于 iPad 类、手表类上面毕竟他们也能打电话。 interface 来约束 class只要 class 实现了 interface 规定的属性或方法就行了没有继承那么多条条框框非常灵活。 约束构造函数和静态属性 使用 implements 只能约束类实例上的属性和方法要约束构造函数和静态属性需要这么写。 以我们上文提过的 Circl 类为例 interface CircleStatic {new (radius: number): voidpi: number }const Circle: CircleStatic class Circle {static pi: 3.14public radius: numberpublic constructor(radius: number) {this.radius radius} }未定义静态属性 pi会报错 interface CircleStatic {new (radius: number): voidpi: number }const Circle: CircleStatic class Circle {// Property pi is missing in type typeof Circle but required in type CircleStatic.public radius: numberpublic constructor(radius: number) {this.radius radius} }constructor 入参类型不对会报错 interface CircleStatic {new (radius: number): voidpi: number }const Circle: CircleStatic class Circle {// Type typeof Circle is not assignable to type CircleStatic. Types of parameters radius and radius are incompatible. Type number is not assignable to type string.static pi: 3.14public radius: numberpublic constructor(radius: string) {this.radius radius} }枚举 在任何项目开发中我们都会遇到定义常量的情况常量就是指不会被改变的值。 TS 中我们使用 const 来声明常量但是有些取值是在一定范围内的一系列常量比如一周有七天比如方向分为上下左右四个方向。 这时就可以使用枚举Enum来定义。 基本使用 enum Direction {Up,Down,Left,Right, }这样就定义了一个数字枚举他有两个特点 数字递增反向映射 枚举成员会被赋值为从 0 开始递增的数字 console.log(Direction.Up) // 0 console.log(Direction.Down) // 1 console.log(Direction.Left) // 2 console.log(Direction.Right) // 3枚举会对枚举值到枚举名进行反向映射 console.log(Direction[0]) // Up console.log(Direction[1]) // Down console.log(Direction[2]) // Left console.log(Direction[3]) // Right如果枚举第一个元素赋有初始值就会从初始值开始递增 enum Direction {Up 6,Down,Left,Right, }console.log(Direction.Up) // 6 console.log(Direction.Down) // 7 console.log(Direction.Left) // 8 console.log(Direction.Right) // 9反向映射的原理 枚举是如何做到反向映射的呢我们不妨来看一下被编译后的代码 var Direction ;(function (Direction) {Direction[(Direction[Up] 6)] UpDirection[(Direction[Down] 7)] DownDirection[(Direction[Left] 8)] LeftDirection[(Direction[Right] 9)] Right })(Direction || (Direction {}))主体代码是被包裹在一个自执行函数里封装了自己独特的作用域。 Direction[Up] 6会将 Direction 这个对象的 Up 属性赋值为 6JS 的赋值运算符返回的值是被赋予的值。 Direction[Up] 6 返回 6 // 执行 Direction[Direction[Up] 6] Up;// 相当于执行 Direction[Up] 6 Direction[6] Up这样就实现了枚举的反向映射。 手动赋值 定义一个枚举来管理外卖状态分别有已下单配送中已接收三个状态。 可以这么写 enum ItemStatus {Buy 1,Send,Receive, }console.log(ItemStatus[Buy]) // 1 console.log(ItemStatus[Send]) // 2 console.log(ItemStatus[Receive]) // 3但有时候后端给你返回的数据状态是乱的就需要我们手动赋值。 比如后端说 Buy 是 100Send 是 20Receive 是 1就可以这么写 enum ItemStatus {Buy 100,Send 20,Receive 1, }console.log(ItemStatus[Buy]) // 100 console.log(ItemStatus[Send]) // 20 console.log(ItemStatus[Receive]) // 1别问为什么实际开发中经常会有这种情况发生。 计算成员 枚举中的成员可以被计算比如经典的使用位运算合并权限可以这么写 enum FileAccess {Read 1 1,Write 1 2,ReadWrite Read | Write, }console.log(FileAccess.Read) // 2 - 010 console.log(FileAccess.Write) // 4 - 100 console.log(FileAccess.ReadWrite) // 6 - 110看个实例吧Vue3 源码中的 patchFlags用于标识节点更新的属性。 // packages/shared/src/patchFlags.ts export const enum PatchFlags {TEXT 1, // 动态文本节点CLASS 1 1, // 动态 classSTYLE 1 2, // 动态 stylePROPS 1 3, // 动态属性FULL_PROPS 1 4, // 具有动态 key 属性当 key 改变时需要进行完整的 diff 比较HYDRATE_EVENTS 1 5, // 具有监听事件的节点STABLE_FRAGMENT 1 6, // 子节点顺序不会被改变的 fragmentKEYED_FRAGMENT 1 7, // 带有 key 属或部分子节点有 key 的 fragmentUNKEYED_FRAGMENT 1 8, // 子节点没有 key 的 fragmentNEED_PATCH 1 9, // 非 props 的比较比如 ref 或指令DYNAMIC_SLOTS 1 10, // 动态插槽DEV_ROOT_FRAGMENT 1 11, // 仅供开发时使用表示将注释放在模板根级别的片段HOISTED -1, // 静态节点BAIL -2, // diff 算法要退出优化模式 }字符串枚举 字符串枚举的意义在于提供有具体语义的字符串可以更容易地理解代码和调试。 enum Direction {Up UP,Down DOWN,Left LEFT,Right RIGHT, }const value UP if (value Direction.Up) {// do something }常量枚举 上文的例子使用 const 来定义一个常量枚举 const enum Direction {Up UP,Down DOWN,Left LEFT,Right RIGHT, }const value UP if (value Direction.Up) {// do something }编译出来的 JS 代码会简洁很多提高了性能。 const value UP if (value UP /* Up */) {// do something }不写 const 编译出来是这样的 var Direction ;(function (Direction) {Direction[Up] UPDirection[Down] DOWNDirection[Left] LEFTDirection[Right] RIGHT })(Direction || (Direction {})) const value UP if (value Direction.Up) {// do something }这一堆定义枚举的逻辑会在编译阶段会被删除常量枚举成员在使用的地方被内联进去。 很显然常量枚举不允许包含计算成员不然怎么叫常量呢 const enum Test {A li.length, // const enum member initializers must be constant expressions. }这么写直接报错。 总结一下常量枚举可以避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问。 小结 枚举的意义在于可以定义一些带名字的常量集合清晰地表达意图和语义更容易地理解代码和调试。 常用于和后端联调时区分后端返回的一些代表状态语义的数字或字符串降低阅读代码时的心智负担。 类型推论 TypeScript 里在有些没有明确指出类型的地方类型推论会帮助提供类型。 这种推断发生在初始化变量和成员设置默认参数值和决定函数返回值时。 定义时不赋值 let aa 18 a li定义时不赋值就会被 TS 自动推导成 any 类型之后随便怎么赋值都不会报错。 初始化变量 例如 let userName li因为赋值的时候赋的是一个字符串类型所以 TS 自动推导出 userName 是 string 类型。 这个时候再更改 userName 时就必须是 string 类型是其他类型就报错比如 let userName li userName function () {// Type () void is not assignable to type string. }设置默认参数值 函数设置默认参数时也会有自动推导 比如定义一个打印年龄的函数默认值是 18 function printAge(num 18) {console.log(num)return num }那么 TS 会自动推导出 printAge 的入参类型传错了类型会报错。 function printAge(num 18) {console.log(num)return num }printAge(18) // Argument of type string is not assignable to parameter of type number.决定函数返回值 决定函数返回值时 TS 也会自动推导出返回值类型。 比如一个函数不写返回值 function welcome() {console.log(hello) }TS 自动推导出返回值是 void 类型 再比如上文的 printAge 函数TS 会自动推导出返回值是 number 类型。 如果我们给 printAge 函数的返回值定义为 string 类型看看会发生什么。 function printAge(num 18) {console.log(num)return num }interface PrintAge {(num: number): string }const printAge1: PrintAge printAge // Type (num?: number) number is not assignable to type PrintAge. Type number is not assignable to type string.很显然定义的类型和 TS 自动推导出的类型冲突报错。 最佳通用类型 当需要从几个表达式中推断类型时候会使用这些表达式的类型来推断出一个最合适的通用类型。比如 let arr [0, 1, null, li] // let arr: (string | number | null)[]又比如 let pets [new Dog(), new Cat()] // let pets: (Dog | Cat)[]虽然 TS 可以推导出最合适的类型但最好还是在写的时候就定义好类型上文的例子我们可以这么写 type arrItem number | string | null let arr: arrItem[] [0, 1, null, li]let pets: Pets[] [new Dog(), new Cat()]小结 类型推论虽然能为我们提供帮助但既然写了 TS除非是函数默认返回类型为 void 这种大家都知道的其他的最好每个地方都定义好类型。 内置类型 JavaScript 中有很多内置对象它们可以直接在 TypeScript 中当做定义好了的类型。 内置对象是指根据标准在全局作用域 global 上存在的对象这里的标准指的是 ECMAcript 和其他环境比如 DOM的标准。 JS 八种内置类型 let name: string lin let age: number 18 let isHandsome: boolean true let u: undefined undefined let n: null null let obj: object { name: lin, age: 18 } let big: bigint 100n let sym: symbol Symbol(lin)ECMAScript 的内置对象 比如Array、Date、Error 等 const nums: Arraynumber [1, 2, 3]const date: Date new Date()const err: Error new Error(Error!)const reg: RegExp /abc/Math.pow(2, 9)以 Array 为例按住 comand/ctrl再鼠标左键点击一下就能跳转到类型声明的地方。 可以看到Array 这个类型是用 interface 定义的有多个不同版本的 .d.ts 文件声明了这个类型。 在 TS 中重复声明一个 interface会把所有的声明全部合并这里所有的 .d.ts 文件合并出来的 Array 接口就组合成了 Array 内置类型的全部属性和功能。 再举个例子 DOM 和 BOM 比如 HTMLElement、NodeList、MouseEvent 等 let body: HTMLElement document.bodylet allDiv: NodeList document.querySelectorAll(div)document.addEventListener(click, (e: MouseEvent) {e.preventDefault()// Do something })TS 核心库的定义文件 TypeScript 核心库的定义文件[9]中定义了所有浏览器环境需要用到的类型并且是预置在 TypeScript 中的。 比如 Math.pow 的类型定义如下 interface Math {/*** Returns the value of a base expression taken to a specified power.* param x The base value of the expression.* param y The exponent value of the expression.*/pow(x: number, y: number): number }又比如addEventListener 的类型定义如下 interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {addEventListener(type: string, listener: (ev: MouseEvent) any, useCapture?: boolean): void }浅尝辄止知道在哪里定义就行真要去分析一些 Web Api 的类型实现是很费精力的。 TS 进阶 这一部分的内容就需要费点脑细胞了毕竟学习一门语言还是没那么容易的最好把基础的内容都理解透彻之后再来学进阶。 高级类型一 高级类型分一和二两部分一的部分不需要理解泛型也能理解二的部分需要理解泛型之后才能理解所以二被拆分到后面去了。 联合类型 如果希望一个变量可以支持多种类型就可以用联合类型union types来定义。 例如一个变量既支持 number 类型又支持 string 类型就可以这么写 let num: number | stringnum 8 num eight联合类型大大提高了类型的可扩展性但当 TS 不确定一个联合类型的变量到底是哪个类型的时候只能访问他们共有的属性和方法。 交叉类型 如果要对对象形状进行扩展可以使用交叉类型 。 比如 Person 有 name 和 age 的属性而 Student 在 name 和 age 的基础上还有 grade 属性就可以这么写 interface Person {name: stringage: number }type Student Person { grade: number }这和类的继承是一模一样的这样 Student 就继承了 Person 上的属性。 联合类型 | 是指可以取几种类型中的任意一种而交叉类型 是指把几种类型合并起来。 交叉类型和 interface 的 extends 非常类似都是为了实现对象形状的组合和扩展。 类型别名type 类型别名type aliase听名字就很好理解就是给类型起个别名。 就像 NBA 球员 扬尼斯-阿德托昆博名字太长难记我们叫他字母哥。 就像我们项目中配置 alias不用写相对路径就能很方便地引入文件 import componentA from ../../../../components/componentA/index.vue 变成 import componentA from /components/componentA/index.vue类型别名用 type 关键字来书写有了类型别名我们书写 TS 的时候可以更加方便简洁。 比如下面这个例子getName 这个函数接收的参数可能是字符串可能是函数就可以这么写。 type Name string type NameResolver () string type NameOrResolver Name | NameResolver // 联合类型 function getName(n: NameOrResolver): Name {if (typeof n string) {return n} else {return n()} }这样调用时传字符串和函数都可以。 getName(li) getName(() li)如果传的格式有问题就会提示。 类型别名会给一个类型起个新名字。类型别名有时和接口很像但是可以作用于原始值联合类型元组以及其它任何你需要手写的类型。-- TS 文档 类型别名的用法如下 type Name string // 基本类型type arrItem number | string // 联合类型const arr: arrItem[] [1, 2, 3]type Person {name: Name }type Student Person { grade: number } // 交叉类型type Teacher Person { major: string }type StudentAndTeacherList [Student, Teacher] // 元组类型const list: StudentAndTeacherList [{ name: li, grade: 100 },{ name: liu, major: Chinese }, ]type 和 interface 的区别 比如下面这个例子可以用 type也可以用 interface。 interface Person {name: stringage: number }const person: Person {name: li,age: 18, }type Person {name: stringage: number }const person: Person {name: li,age: 18, }那 type 和 interface 难道都可以随便用总得有个区别吧。 两者相同点 都可以定义一个对象或函数都允许继承 都可以定义一个对象或函数 定义对象上文已经说了我们来看一下如何定义函数。 type addType (num1: number, num2: number) numberinterface addType {(num1: number, num2: number): number } // 这两种写法都可以定义函数类型const add: addType (num1, num2) {return num1 num2 }都允许继承 我们定义一个 Person 类型和 Student 类型Student 继承自 Person可以有下面四种方式 // interface 继承 interface interface Person {name: string } interface Student extends Person {grade: number }const person: Student {name: lin,grade: 100, }// type 继承 type type Person {name: string } type Student Person { grade: number } // 用交叉类型// interface 继承 type type Person {name: string }interface Student extends Person {grade: number }// type 继承 interface interface Person {name: string }type Student Person { grade: number } // 用交叉类型interface 使用 extends 实现继承 type 使用交叉类型实现继承 两者不同点 interface接口 是 TS 设计出来用于定义对象类型的可以对对象的形状进行描述。type 是类型别名用于给各种类型定义别名让 TS 写起来更简洁、清晰。type 可以声明基本类型、联合类型、交叉类型、元组interface 不行interface 可以合并重复声明type 不行 合并重复声明 interface Person {name: string }interface Person {// 重复声明 interface就合并了age: number }const person: Person {name: lin,age: 18, }重复声明 type 就报错了 type Person {name: string }type Person {// Duplicate identifier Personage: number }const person: Person {name: lin,age: 18, }这两者的区别说了这么多其实本不该把这两个东西拿来做对比他们俩是完全不同的概念。 interface 是接口用于描述一个对象。 type 是类型别名用于给各种类型定义别名让 TS 写起来更简洁、清晰。 只是有时候两者都能实现同样的功能才会经常被混淆 平时开发中一般使用组合或者交叉类型的时候用 type。 一般要用类的 extends 或 implements 时用 interface。 其他情况比如定义一个对象或者函数就看你心情了。 类型保护 如果有一个 getLength 函数入参是联合类型 number | string返回入参的 length function getLength(arg: number | string): number {return arg.length }从上文可知这么写会报错因为 number 类型上没有 length 属性。 这个时候类型保护Type Guards出现了可以使用 typeof 关键字判断变量的类型。 我们把 getLength 方法改造一下就可以精准地获取到 string 类型的 length 属性了 function getLength(arg: number | string): number {if (typeof arg string) {return arg.length} else {return arg.toString().length} }之所以叫类型保护就是为了能够在不同的分支条件中缩小范围这样我们代码出错的几率就大大降低了。 类型断言 上文的例子也可以使用类型断言来解决。 类型断言语法 值 as 类型使用类型断言来告诉 TS我开发者比你编译器更清楚这个参数是什么类型你就别给我报错了 function getLength(arg: number | string): number {const str arg as stringif (str.length) {return str.length} else {const number arg as numberreturn number.toString().length} }注意类型断言不是类型转换把一个类型断言成联合类型中不存在的类型会报错。 比如 function getLength(arg: number | string): number {return (arg as number[]).length// Conversion of type string | number to type number[] may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to unknown first.// Type number is not comparable to type number[]. }字面量类型 有时候我们需要定义一些常量就需要用到字面量类型比如 type ButtonSize mini | small | normal | largetype Sex 男 | 女这样就只能从这些定义的常量中取值乱取值会报错 type Sex 男 | 女const sex: Sex 不男不女 // Type 不男不女 is not assignable to type Sex.泛型 泛型是 TS 比较难理解的部分拿下了泛型对 TS 的理解就又上了一个台阶对后续深入学习帮助很大。 为什么需要泛型 如果你看过 TS 文档一定看过这样两段话 软件工程中我们不仅要创建一致的定义良好的 API同时也要考虑可重用性。组件不仅能够支持当前的数据类型同时也能支持未来的数据类型这在创建大型系统时为你提供了十分灵活的功能。 在像 C# 和 Java 这样的语言中可以使用泛型来创建可重用的组件一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。 简直说的就不是人话你确定初学者看得懂 我觉得初学者应该要先明白为什么需要泛型这个东西它解决了什么问题而不是看这种拗口的定义。 我们还是先来看这样一个例子体会一下泛型解决的问题吧。 定义一个 print 函数这个函数的功能是把传入的参数打印出来再返回这个参数传入参数的类型是 string函数返回类型为 string。 function print(arg: string): string {console.log(arg)return arg }现在需求变了我还需要打印 number 类型怎么办 可以使用联合类型来改造 function print(arg: string | number): string | number {console.log(arg)return arg }现在需求又变了我还需要打印 string 数组、number 数组甚至任何类型怎么办 有个笨方法支持多少类型就写多少联合类型。 或者把参数类型改成 any。 function print(arg: any): any {console.log(arg)return arg }且不说写 any 类型不好毕竟在 TS 中尽量不要写 any。 而且这也不是我们想要的结果只能说传入的值是 any 类型输出的值是 any 类型传入和返回并不是统一的。 这么写甚至还会出现 bug const res: string print(123)定义 string 类型来接收 print 函数的返回值返回的是个 number 类型TS 并不会报错提示我们。 这个时候泛型就出现了它可以轻松解决输入输出要一致的问题。 注意泛型不是为了解决这一个问题设计出来的泛型还解决了很多其他问题这里是通过这个例子来引出泛型。 泛型基本使用 处理函数参数 我们使用泛型来解决上文的问题。 泛型的语法是 里写类型参数一般可以用 T 来表示。 function printT(arg: T): T {console.log(arg)return arg }这样我们就做到了输入和输出的类型统一且可以输入输出任何类型。 泛型中的 T 就像一个占位符、或者说一个变量在使用的时候可以把定义的类型像参数一样传入它可以原封不动地输出。 泛型的写法对前端工程师来说是有些古怪比如 T 但记住就好只要一看到 就知道这是泛型。 我们在使用的时候可以有两种方式指定类型。 定义要使用的类型TS 类型推断自动推导出类型 printstring(hello) // 定义 T 为 stringprint(hello) // TS 类型推断自动推导类型为 string我们知道type 和 interface 都可以定义函数类型也用泛型来写一下type 这么写 type Print T(arg: T) T const printFn: Print function print(arg) {console.log(arg)return arg }interface 这么写 interface IprintT {(arg: T): T }function printT(arg: T) {console.log(arg)return arg }const myPrint: Iprintnumber print默认参数 如果要给泛型加默认参数可以这么写 interface IprintT number {(arg: T): T }function printT(arg: T) {console.log(arg)return arg }const myPrint: Iprint print这样默认就是 number 类型了怎么样是不是感觉 T 就如同函数参数一样呢 处理多个函数参数 现在有这么一个函数传入一个只有两项的元组交换元组的第 0 项和第 1 项返回这个元组。 function swap(tuple) {return [tuple[1], tuple[0]] }这么写我们就丧失了类型用泛型来改造一下。 我们用 T 代表第 0 项的类型用 U 代表第 1 项的类型。 function swapT, U(tuple: [T, U]): [U, T] {return [tuple[1], tuple[0]] }这样就可以实现了元组第 0 项和第 1 项类型的控制。 函数副作用操作 泛型不仅可以很方便地约束函数的参数类型还可以用在函数执行副作用操作的时候。 比如我们有一个通用的异步请求方法想根据不同的 url 请求返回不同类型的数据。 function request(url: string) {return fetch(url).then((res) res.json()) }调一个获取用户信息的接口 request(user/info).then((res) {console.log(res) })这时候的返回结果 res 就是一个 any 类型非常讨厌。 我们希望调用 API 都清晰的知道返回类型是什么数据结构就可以这么做 interface UserInfo {name: stringage: number }function requestT(url: string): PromiseT {return fetch(url).then((res) res.json()) }requestUserInfo(user/info).then((res) {console.log(res) })这样就能很舒服地拿到接口返回的数据类型开发效率大大提高 约束泛型 假设现在有这么一个函数打印传入参数的长度我们这么写 function printLengthT(arg: T): T {console.log(arg.length) // Property length does not exist on type T.(return arg }因为不确定 T 是否有 length 属性会报错 那么现在我想约束这个泛型一定要有 length 属性怎么办 可以和 interface 结合来约束类型。 interface ILength {length: number }function printLengthT extends ILength(arg: T): T {console.log(arg.length)return arg }这其中的关键就是 T extends ILength让这个泛型继承接口 ILength这样就能约束泛型。 我们定义的变量一定要有 length 属性比如下面的 str、arr 和 obj才可以通过 TS 编译。 const str printLength(lin) const arr printLength([1, 2, 3]) const obj printLength({ length: 10 })这个例子也再次印证了 interface 的 duck typing。 只要你有 length 属性都符合约束那就不管你是 strarr 还是 obj都没问题。 当然我们定义一个不包含 length 属性的变量比如数字就会报错 const num printLength(18) // Argument of type number is not assignable to parameter of type ILength.泛型的一些应用 使用泛型可以在定义函数、接口或类的时候不预先指定具体类型而是在使用的时候再指定类型。 泛型约束类 定义一个栈有入栈和出栈两个方法如果想入栈和出栈的元素类型统一就可以这么写 class StackT {private data: T[] []push(item: T) {return this.data.push(item)}pop(): T | undefined {return this.data.pop()} }在定义实例的时候写类型比如入栈和出栈都要是 number 类型就这么写 const s1 new Stacknumber()这样入栈一个字符串就会报错 s1.push(li) // Argument of type string is not assignable to parameter of type number.这是非常灵活的如果需求变了入栈和出栈都要是 string 类型在定义实例的时候改一下就好了 const s1 new Stackstring()特别注意的是泛型无法约束类的静态成员。 给 pop 方法定义 static 关键字就报错了 class StackT {private data: T[] []push(item: T) {return this.data.push(item)}static pop(): T | undefined {// Static members cannot reference class type parametersreturn this.data.pop()} }泛型约束接口 使用泛型也可以对 interface 进行改造让 interface 更灵活。 interface IKeyValueT, U {key: Tvalue: U }const k1: IKeyValuenumber, string { key: 18, value: lin } const k2: IKeyValuestring, number { key: lin, value: 18 }泛型定义数组 定义一个数组我们之前是这么写的 const arr: number[] [1, 2, 3]现在这么写也可以 const arr: Arraynumber [1, 2, 3]小结 泛型Generics从字面上理解泛型就是一般的广泛的。 泛型是指在定义函数、接口或类的时候不预先指定具体类型而是在使用的时候再指定类型。 泛型中的 T 就像一个占位符、或者说一个变量在使用的时候可以把定义的类型像参数一样传入它可以原封不动地输出。 泛型在成员之间提供有意义的约束这些成员可以是函数参数、函数返回值、类的实例成员、类的方法等。 #mermaid-svg-ajAQ13xNbPKGMBE9 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .error-icon{fill:#552222;}#mermaid-svg-ajAQ13xNbPKGMBE9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ajAQ13xNbPKGMBE9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .marker.cross{stroke:#333333;}#mermaid-svg-ajAQ13xNbPKGMBE9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ajAQ13xNbPKGMBE9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .cluster-label text{fill:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .cluster-label span{color:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .label text,#mermaid-svg-ajAQ13xNbPKGMBE9 span{fill:#333;color:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .node rect,#mermaid-svg-ajAQ13xNbPKGMBE9 .node circle,#mermaid-svg-ajAQ13xNbPKGMBE9 .node ellipse,#mermaid-svg-ajAQ13xNbPKGMBE9 .node polygon,#mermaid-svg-ajAQ13xNbPKGMBE9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ajAQ13xNbPKGMBE9 .node .label{text-align:center;}#mermaid-svg-ajAQ13xNbPKGMBE9 .node.clickable{cursor:pointer;}#mermaid-svg-ajAQ13xNbPKGMBE9 .arrowheadPath{fill:#333333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ajAQ13xNbPKGMBE9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ajAQ13xNbPKGMBE9 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ajAQ13xNbPKGMBE9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ajAQ13xNbPKGMBE9 .cluster text{fill:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 .cluster span{color:#333;}#mermaid-svg-ajAQ13xNbPKGMBE9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ajAQ13xNbPKGMBE9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 泛型的好处 函数和类可以轻松地支持多种类型增强程序的拓展性 不必写冗长的联合类型增强代码的可读性 灵活控制类型之间的约束 高级类型二 索引类型 从对象中抽取一些属性的值,然后拼接成数组可以这么写 const userInfo {name: li,age: 18, }function getValues(userInfo: any, keys: string[]) {return keys.map((key) userInfo[key]) }// 抽取指定属性的值 console.log(getValues(userInfo, [name, age])) // [li, 18] // 抽取obj中没有的属性: console.log(getValues(userInfo, [sex, outlook])) // [undefined, undefined]虽然 obj 中并不包含 sex 和 outlook 属性,但 TS 编译器并未报错 此时使用 TS 索引类型,对这种情况做类型约束实现动态属性的检查。 理解索引类型需先理解 keyof索引查询、T[K]索引访问 和 extends (泛型约束)。 keyof索引查询 keyof 操作符可以用于获取某种类型的所有键其返回类型是联合类型。 interface IPerson {name: stringage: number }type Test keyof IPerson // name | age上面的例子Test 类型变成了一个字符串字面量。 T[K]索引访问 T[K]表示接口 T 的属性 K 所代表的类型 interface IPerson {name: stringage: number }let type1: IPerson[name] // string let type2: IPerson[age] // numberextends (泛型约束) T extends U表示泛型变量可以通过继承某个类型获得某些属性之前讲过复习一下 interface ILength {length: number }function printLengthT extends ILength(arg: T): T {console.log(arg.length)return arg }这样入参就一定要有 length 属性比如 str、arr、obj 都可以 num 就不行。 const str printLength(li) const arr printLength([1, 2, 3]) const obj printLength({ length: 10 })const num printLength(10) // 报错Argument of type number is not assignable to parameter of type ILength检查动态属性 对索引类型的几个概念了解后,对 getValue 函数进行改造实现对象上动态属性的检查。 改造前 const userInfo {name: li,age: 18, }function getValues(userInfo: any, keys: string[]) {return keys.map((key) userInfo[key]) }定义泛型 T、K用于约束 userInfo 和 keys为 K 增加一个泛型约束,使 K 继承 userInfo 的所有属性的联合类型, 即K extends keyof T 改造后 function getValuesT, K extends keyof T(userInfo: T, keys: K[]): T[K][] {return keys.map((key) userInfo[key]) }这样当我们指定不在对象里的属性时就会报错 getValues(userInfo, [sex, outlook]) // Type outlook is not assignable to type name | age映射类型 TS 允许将一个类型映射成另外一个类型。 in 介绍映射类型之前先介绍一下 in 操作符用来对联合类型实现遍历。 type Person name | school | majortype Obj {[p in Person]: string }Partial PartialT将T的所有属性映射为可选的例如 interface IPerson {name: stringage: number }let p1: IPerson {name: li,age: 18, }使用了 IPerson 接口就一定要传 name 和 age 属性。 使用 Partial 改造一下就可以变成可选属性 interface IPerson {name: stringage: number }type IPartial PartialIPersonlet p1: IPartial {}Partial 原理 Partial 的实现用到了 in 和 keyof /*** Make all properties in T optional*/ type PartialT {[P in keyof T]?: T[P] }[P in keyof T]遍历T上的所有属性?:设置为属性为可选的T[P]设置类型为原来的类型 Readonly ReadonlyT将T的所有属性映射为只读的例如 interface IPerson {name: stringage: number }type IReadOnly ReadonlyIPersonlet p1: IReadOnly {name: li,age: 18, }Readonly 原理 和 Partial 几乎完全一样 /*** Make all properties in T readonly*/ type ReadonlyT {readonly [P in keyof T]: T[P] }[P in keyof T]遍历T上的所有属性readonly设置为属性为可选的T[P]设置类型为原来的类型 Pick Pick用于抽取对象子集挑选一组属性并组成一个新的类型例如 interface IPerson {name: stringage: numbersex: string }type IPick PickIPerson, name | agelet p1: IPick {name: lin,age: 18, }这样就把 name 和 age 从 IPerson 中抽取出来。 Pick 原理 /*** From T, pick a set of properties whose keys are in the union K*/ type PickT, K extends keyof T {[P in K]: T[P] }Pick 映射类型有两个参数: 第一个参数 T表示要抽取的目标对象第二个参数 K具有一个约束K 一定要来自 T 所有属性字面量的联合类型 Record 上面三种映射类型官方称为同态,意思是只作用于 obj 属性而不会引入新的属性。 Record 是会创建新属性的非同态映射类型。 interface IPerson {name: stringage: number }type IRecord Recordstring, IPersonlet personMap: IRecord {person1: {name: lin,age: 18,},person2: {name: liu,age: 25,}, }Record 原理 /*** Construct a type with a set of properties K of type T*/ type RecordK extends keyof any, T {[P in K]: T }Record 映射类型有两个参数: 第一个参数可以传入继承于 any 的任何值第二个参数作为新创建对象的值被传入。 条件类型 T extends U ? X : Y //若类型 T 可被赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型Exclude 和 Extract 的实现就用到了条件类型。 Exclude Exclude 意思是不包含ExcludeT, U 会返回 联合类型 T 中不包含 联合类型 U 的部分。 type Test Excludea | b | c, a // type Test b | cExclude 原理 /*** Exclude from T those types that are assignable to U*/ type ExcludeT, U T extends U ? never : Tnever表示一个不存在的类型never与其他类型的联合后为其他类型 type Test string | number | never // type Test string | numberExtract ExtractT, U提取联合类型 T 和联合类型 U 的所有交集。 type Test Extractkey1 | key2, key1 // type Test key1Extract 原理 /*** Extract from T those types that are assignable to U*/ type ExtractT, U T extends U ? T : never懂了 Exclude也就懂了 Extract。 工具类型Utility Types 为了方便开发者使用 TypeScript 内置了一些常用的工具类型。 上文介绍的索引类型、映射类型和条件类型都是工具类型。 除了上文介绍的再介绍一些常用的毕竟工具函数遇到了去查就行死记硬背就太枯燥了熟能生巧写多了自然就熟悉了。 Omit OmitT, U从类型 T 中剔除 U 中的所有属性。 interface IPerson {name: stringage: number }type IOmit OmitIPerson, age // type IOmit { // name: string; // }这样就剔除了 IPerson 上的 age 属性。 Omit 原理 /*** Construct a type with the properties of T except for those in type K.*/ type OmitT, K extends keyof any PickT, Excludekeyof T, KPick 用于挑选一组属性并组成一个新的类型Omit 是剔除一些属性留下剩余的他们俩有点相反的感觉。 那么就可以用 Pick 和 Exclude 实现 Omit。 当然也可以不用 Pick 实现 type Omit2T, K extends keyof any {[P in Excludekeyof T, K]: T[P] }NonNullable NonNullableT 用来过滤类型中的 null 及 undefined 类型。 type T0 NonNullablestring | number | undefined // string | number type T1 NonNullablestring[] | null | undefined // string[]NonNullable 原理 /*** Exclude null and undefined from T*/ type NonNullableT T extends null | undefined ? never : Tnever表示一个不存在的类型never与其他类型的联合后为其他类型 Parameters Parameters 获取函数的参数类型将每个参数类型放在一个元组中。 type T1 Parameters() string // []type T2 Parameters(arg: string) void // [string]type T3 Parameters(arg1: string, arg2: number) void // [arg1: string, arg2: number]Parameters 原理 /*** Obtain the parameters of a function type in a tuple*/ type ParametersT extends (...args: any) any T extends (...args: infer P) any ? P : never在条件类型语句中可以用 infer 声明一个类型变量并且对它进行使用。 Parameters首先约束参数T必须是个函数类型判断T是否是函数类型如果是则使用infer P暂时存一下函数的参数类型后面的语句直接用 P 即可得到这个类型并返回否则就返回never ReturnType ReturnType 获取函数的返回值类型。 type T0 ReturnType() string // stringtype T1 ReturnType(s: string) void // voidReturnType 原理 /*** Obtain the return type of a function type*/ type ReturnTypeT extends (...args: any) any T extends (...args: any) infer R ? R : any懂了 Parameters也就懂了 ReturnType ReturnType首先约束参数T必须是个函数类型判断T是否是函数类型如果是则使用infer R暂时存一下函数的返回值类型后面的语句直接用 R 即可得到这个类型并返回否则就返回any 类型体操是什么 在本节中我们熟悉了很多工具类型的作用和原理其实已经在不知不觉中做了一些类型体操了 TypeScript 高级类型会根据类型参数求出新的类型这个过程会涉及一系列的类型计算逻辑这些类型计算逻辑就叫做类型体操。当然这并不是一个正式的概念只是社区的戏称因为有的类型计算逻辑是比较复杂的。 想一想我们之前研究的这些工具类型都是在对类型做计算返回新的类型啊。 Ts 是一门图灵完备的编程语言即类型的可编码化可以通过代码逻辑生成指定的各种类型基于这点才会有各种类型体操。 个人觉得 TS 类型体操这种东西我们这些搬砖人不必像刷算法一样刻意去训练浅尝辄止了解即可。 TS 声明文件 declare 当使用第三方库时很多三方库不是用 TS 写的我们需要引用它的声明文件才能获得对应的代码补全、接口提示等功能。 比如在 TS 中直接使用 Vue就会报错。 这时我们可以使用 declare 关键字来定义 Vue 的类型简单写一个模拟一下 interface VueOption {el: stringdata: any }declare class Vue {options: VueOptionconstructor(options: VueOption) }const app new Vue({el: #app,data: {message: Hello Vue!,}, })这样就不会报错了使用 declare 关键字相当于告诉 TS 编译器这个变量Vue的类型已经在其他地方定义了你直接拿去用别报错。 需要注意的是declare class Vue 并没有真的定义一个类只是定义了类 Vue 的类型仅仅会用于编译时的检查在编译结果中会被删除。它编译结果是 const app new Vue({el: #app,data: {message: Hello Vue!,}, }).d.ts 通常我们会把声明语句放到一个单独的文件Vue.d.ts中这就是声明文件以 .d.ts 为后缀。 // src/Vue.d.ts interface VueOption {el: stringdata: any }declare class Vue {options: VueOptionconstructor(options: VueOption) }// src/index.ts const app new Vue({el: #app,data: {message: Hello Vue!,}, })一般来说ts 会解析项目中所有的 *.ts 文件当然也包含以 .d.ts 结尾的文件。所以当我们将 Vue.d.ts 放到项目中时其他所有 *.ts 文件就都可以获得 Vue 的类型定义了。 /path/to/project ├── src | ├── index.ts | └── Vue.d.ts └── tsconfig.json使用三方库 那么当我们使用三方库的时候是不是所有的三方库都要写一大堆 decare 的文件呢 答案是不一定要看社区里有没有这个三方库的 TS 类型包一般都有。 社区使用 types 统一管理第三方库的声明文件是由 DefinitelyTyped 这个组织统一管理的 比如安装 lodash 的类型包 npm install types/lodash -D只需要安装了就可以在 TS 里正常使用 lodash 了别的啥也不用做。 当然如果一个库本来就是 TS 写的就不用担心类型文件的问题比如 Vue3。 自己写声明文件 比如你以前写了一个请求小模块 myFetch代码如下 function myFetch(url, method, data) {return fetch(url, {body: data ? JSON.stringify(data) : ,method,}).then((res) res.json()) }myFetch.get (url) {return myFetch(url, GET) }myFetch.post (url, data) {return myFetch(url, POST, data) }export default myFetch现在新项目用了 TS 了要在新项目中继续用这个 myFetch你有两种选择 用 TS 重写 myFetch新项目引重写的 myFetch直接引 myFetch 给它写声明文件 如果选择第二种方案就可以这么做 type HTTPMethod GET | POST | PUT | DELETEdeclare function myFetchT any(url: string, method: HTTPMethod, data?: any): PromiseTdeclare namespace myFetch {// 使用 namespace 来声明对象下的属性和方法const get: T any(url: string) PromiseTconst post: T any(url: string, data: any) PromiseT }比较麻烦的是需要配置才行可以有两种选择 创建一个 node_modules/types/myFetch/index.d.ts 文件存放 myFetch 模块的声明文件。这种方式不需要额外的配置但是 node_modules 目录不稳定代码也没有被保存到仓库中无法回溯版本有不小心被删除的风险故不太建议用这种方案一般只用作临时测试。创建一个 types 目录专门用来管理自己写的声明文件将 myFetch 的声明文件放到 types/myFetch/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 paths 和 baseUrl 字段。// tsconfig.json {compilerOptions: {module: commonjs,baseUrl: ./,paths: {*: [types/*]}} }感觉直接用 TS 重写比给老项目写声明文件更好这样就不用专门维护类型模块了。 平时开发中用到的很多库现在基本上都用 TS 重写了。
http://www.w-s-a.com/news/886676/

相关文章:

  • 关注城市建设网站居众装饰
  • 网站建设的语言优化企业网站
  • 成都旅游网站建设规划女性门户资讯类网站织梦dedecms模板
  • 二手车为什么做网站网站建设合作合同范文
  • 网站建设维护和网页设计做网站都需要服务器吗
  • 成都网站设计报告书系统平台
  • 怎样进行网站推广wordpress微博图床
  • 做一个平台 网站服务器搭建网架公司股价
  • 链家在线网站是哪个公司做的一个虚拟主机做2个网站
  • 网站开发实训报告模板学校网站建设计划
  • 免费手机网站制作方法什么事网站开发
  • 我们的爱情网站制作阿里云wordpress配置
  • 电脑网站页面怎么调大小唐山网站建设技术外包
  • 科威网络做网站怎么样wordpress分页样式
  • 泰安公司网站建设自助建站程序
  • 网站建设工程设计图建网站怎样往网站传视频
  • 做网站月入企业网站建设运营
  • 网站建设中的ftp地址公众号微官网
  • 手机wap网站开发与设计app开发公司电话
  • 网站页脚代码大沥网站开发
  • 重庆网站制作公司 广州天成网络技术有限公司
  • 佛山网站改版wordpress 是否有后门
  • 如何承接网站建设外包wordpress产品布局
  • 洛阳建站洛阳市网站建设视觉设计专业
  • 婚恋网站建设分析网站建设硬件需求
  • 北京做网站电话wordpress如何换图片
  • 电影网站做cpa深圳信息网
  • 单县网站建设优化大师电脑版官网
  • 番禺区住房和建设局物业网站浦东新区网站设计
  • 外贸网站外包WordPress仿牌