你还不知道 TypeScript 有什么好处吗?

4,246 阅读29分钟

一、为了获得更好的开发体验,解决js中一些难以解决的问题

TypeScript是什么?我们为什么要使用TypeScript?

光说不写太抽象,我还是举个例子吧,看以下这段代码

  const origin = [{id:101, name:'ts'}]
  const findUserNameById = (arr, id) => {     
       let result = arr.filters(item => { 
          return id === item.id     
       })
       return result;  
  }  
  let obj = findUserNmaeById(origin, '101')
  console.log(obj[0].name + 'is very good !!!')

以上这段代码会输出什么?有的小伙伴可能发现了问题,但是你又能发现几个呢?

  1. Uncaught ReferenceError: findUserNmaeById is not defined
  2. arr.filters is not a function
  3. Uncaught TypeError: Cannot read property 'name' of undefined
  4. 更深的雷:id === item.id,由于调用的时候传入的是 string 类型的,源数据是number类型的,这样你将永远得不到数据;('101' === 101  -> false)

       写代码时候是不是都会遇到这样的问题? 我相信有很多小伙伴和我一样会遇到这些......嗯低级的错误。(拥有了ts,真香,我真的笑出了zhu声~~~)

二、javascript 存在的问题

  1. 使用了不存在的变量函数或者成员;(函数名字写错等等)
  2. 函数返回类型不准确,无法预知的类型;(把不确定的类型,当成确定的类型)
  3. 在使用null或者undefined成员;
  4. 代码的 可阅读性可维护性,没法控制;

三、javascript 的原罪

  1. js语言本身的特性,决定了该语言无法适应大型复杂项目;
  2. 弱类型,某个变量随时更换类型,在哪里重新赋值改变了类型你无法预测;
  3. 解释性语言,解释一行,运行一行。错误发生的时间点是运行时;
  4. 前端开发中,大部分时间都在排错。(很多时候,都在花费时间排雷!!!)

什么是TypeScript ?

一、TypeScript (简称TS)

  1. TS是JS的超集,是一个可选的,静态的类型系统。
  2. 类型系统,对所有的标识符(变量、参数、函数、返回值)进行类型检查
  3. 类型检查是在编译时候,运行之前(运行的是编译后的JS代码)
  4. 需要tsc index.ts 转换才能执行,TS不参与任何运行时候的检查。


二、TypeScript 的常识

  • 2012年微软发布
  • anders负责开发TS项目,后来开源了
  • 定位类型检查系统,缩短项目排错时间
  • 有了类型检查之后,无形中增强了面向对象的开发
  • JS可以面向对象开发,但是会遇到很多问题的。(暂不举具例)
  • 使用TS后,可以编写出完善的面向对象代码

三、TS 需要注意的一些项目细节

  1. 假设当前的执行环境是浏览器的环境
  2. 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行。
  3. 一个文件中 let n:number = 1 ,tsc 编译之后形成的js文件中的变量是全局的,所有TS文件的全局变量报错。全局冲突嘛。
  4. 编译的目标代码,是ES3,可配置。
有两种方式更改以上假设:
  • 使用tsc命令行编译的时候,加上选项参数
  • 使用TS配置文件,来更改编译选项(重点学, tsc --init命令会直接生成tsconfig.json文件)
  • 使用了配置文件之后,tsc编译不能再跟上文件名了,如果跟上会忽略配置文件
  • @types/node, @types是官方的第三方库,其中包含了很多对js代码类型的描述。(比如JQ,可以安装@types/jquery, 为jquery库添加类型描述)

TypeScript 正式入门

       正式开始学习typescript之前呢,给小伙伴们安利一个网站,可以在线编译,把TS代码直接对比编译成JS代码,对我们入门学习很有帮助。

www.typescriptlang.org/play/index.…

  •  TS是可选的,TS类型是静态类型,暂时做不到运行时的类型检查,最终运行还是JS代码

一、基本类型

  • number
  • string
  • boolean
  • 数组

let numArr: number[] = [1, 2]; 
let numArr: Array<number> = [1, 2] //这种也叫泛型
  • 对象

let u:object;  // 约束力很弱,赋值函数也不会报错,用得不多。
  • null / undefined

- nullundefined 是所有其它类型的子类型,它们可以赋值给其它类型
- "strictNullChecks": true, 可以更加严格的空类型检查,不让nullundefined 随便赋值,只能赋值给自身
- 随着版本更新,2020-2-20发布的3.8版本有更多新特性、基本类型

二、其它常用类型

  • 联合类型(多种类型,任选其一),比如字面量联合类型,类型联合类型,后面还会学到 & 与操作

- let name:string | undefined = undefined // 如果没有具体赋值,还是可以类型推导。
- 类型保护,如果某个变量进行判断之后,就可以确定是什么类型了。(后面讲怎么触发类型保护,只能触发原始类型)
  • void类型,通常用于约束函数的返回值
  • never类型,通常用于约束函数的返回值,表示该函数永远不可能结束,不会返回值或者函数内死循环。

function throwError(msg :string):never{    
  throw new Error(msg); //到这里就停了    
  console.log('aaaa');  //这里以至于下面都不可能执行了  
}
  • 字面量类型

let gender:'男' | '女'; //从此只能赋值男女   
let arr:[] //永远只能取值为一个空数组   
let user: { name:string, age:number }   
    user = { name:'hrt', age:23 } //强力约束了

  • 元祖类型(Tuple),一个固定长度的数组,并且数组中每一项的类型确定

let tu:[string, number]; //之后赋值必须赋值两项,类型确定
  • any 类型

- any类型:any类型,可以绕过类型检查,因此,any类型的数据,可以赋值给任意类型
- let data:any; 
- let num:number = data
- 会有隐患,不要随意用,如果没有必要,不要使用any类型
  • 基本类型 和其它 常用类型 统称为TS类型
  • 类型别名, 对已知的类型定义别名

- 这个react会经常用到
type User = {   
  name:string  
  age:number
}
//不仅变量可以这样方便的使用,函数返回值也可以方便点,简洁代码
let user: User - 后面还可以通过一些操作符,像&之类的,高级联合类型

type Gender = '男' | '女'
type User = {  
  name:string,  
  age:number 
  gender:Gender //这样可以很方便的维护代码,代码重用
}

三、函数相关的类型约束

  • 实用的函数重载

- 函数重载:在函数实现之前,对调用的多种情况进行声明
- 举例:   
function combine(a:number, b:number):number;   
function combine(a:string, b:string):string;   
function combine(a:number | string, b:number | string):number | string{      
  if(typeof a === 'number' && typeof b === 'number'){  
     return a * b      
  }else if(typeof a === 'string' && typeof b === 'string'){  
     return a + b      
  }      
  throw new Error('a和b 类型必须相同')   
}   
const result = combine("2", '2') //使用的时候,result 就可以具体的推导出类型
  • 可选参数和默认值

- 可选参数,但是要注意放在必须参数后面
- 当然,也可以有默认值 a:number = 1
function combine(a:number = 1, b?:number):number { 
   return a + 0
} 

四、枚举类型

- 定义语法:enum 枚举名{ 枚举字段 }
- 举例使用:
enum Gender {   
  male = '男',   female = '女'
}
let gender:Gender;
gender = Gender.male
gender = Gender.female

有的小伙伴会想,既然都有类型别名了,我们还需要枚举类型干什么?我们先来一个小案例

需求:某个函数中要通过性别过滤对象,返回所有的特定性别,我们要定义一个特定性别类型

  • 用字面量类型;字面量类型会有问题,出现重复的代码,如 let gender: '男' | '女';在调用函数的时候,参数也必须写字面量,会出现硬编码,不好维护;
  • 类型别名可以解决硬编码,type Gender = '男' | '女' ;但是,如果一旦别名类型一改,全部的代码都要改。我们先来看段代码

type Gender = '男' | '女'
let gender:Gender;    
gender = '男' //这里只能选字面量的值
//我们假设后面还有 N 多个 变量/函数 使用类型别名 Gender
//如果Gender改了,改成type Gender = 'boy' | 'girl'; gender后面甚至更多的都要改
//根源是真实的值和逻辑含义产生了混淆,没有分开。真实值一改,全要改了。
//字面量类型不会进入到编译结果

  • 枚举类型,自定义类型,扩展类型

- 枚举类型可以很好的解决上面硬编码的问题
- 举例使用:   
enum Gender {     
  male = '男',     
  female = '女'  
}   
let gender:Gender;   
gender = Gender.male   
gender = Gender.female
- 因此,使用的时候是使用逻辑名称,赋值的时候还是字面量
- 如果逻辑名称改了,f2重构就可以了,一改全改,编码硬编码
- 枚举参与编译,会出现在编译结果中,编译成一个对象。以前的类型别名,不会参数到编译中的

  • 枚举细节规则

  1. 枚举的字段值,只能使用字符串或者数字
  2. 枚举的字段名称,要符合变量的命名规则,比如不能以数字开头
  3. 数字枚举的时候,值会自动自增,比如第一个写了1,后面不写,后面就自增1,如果全都不写,第一个就是0,后面自增

enum Level {      
  level1 = 1,      
  level2,      
  level3,   
}

注意细节:

  1. 被数字枚举约束的变量,可以直接赋值为数子,如:let lev:Level = 1;原因是因为位枚举。
  2. 数据枚举的编译结果,和字符串编译结果有差异,将来枚举数字的时候千万要注意

最佳事件:

  1. 尽量不要在一个枚举中,即出现字符串字段值,又出现了数字字段值;
  2. 使用枚举时,尽量使用枚举的名字字段,不要使用真实值,避免硬编码;
  3. 能使用枚举,就不要使用类型别名,因为会出现上面我们分析的一改全改的情况;

实战扩展:

针对数字枚举,在一些权限控制下非常的优雅,可扩展性也很强,直接看代码

enum Permission {  
   Read = 1,  //0001     
   Write = 2, //0010        
   Create = 4,//0100        
   Delete = 8 //1000    
}   
// 1.组合权限,使用或运算    
// 0001 | 0010 -> 0011    
let p:Permission = Permission.Read | Permission.Write    
// 2.如何判断是否拥有某个权限, 用与运算    
function hasPermission(target:Permission, per:Permission):boolean {  
   return (target & per) === Permission.Read   
}    
// 判断是不是有读权限    
hasPermission(p, Permission.Read);   
// 3.如何删除某个权限    
// 亦或,相同取0,不同1    
// 0011 ^ 0010 -> 0001 这样就可以删除写权限了;    
p = p ^ Permission.Write

五、接口

什么是接口呢?

interface,接口。TS中的接口,用于约束类,对象,函数的契约(标准)。

举个例子,电源接口,如果两个充电器的电源接口满足相同的标准,就可以互相拿来充电。

只不过,我们以前前端开发的契约标准就是文档形式的。API文档,就是一个契约文档。但是文档作为契约,作为标准的话,是一个弱标准。没有强制按照要求来使用;写代码的时候调用接口时候会写错,完全没有提示。在代码层次的接口约束提醒,就是强约束;常见的代码层次强约束语言,如 java、c# 等;

这样解释还是过于干燥,我们来举一些实例吧;

  • 接口约束对象

interface IUser {   
  name:string,   
  age:number,   
  sayHello?:() => void, //千万不要在这里写实现,这里是定义。
}
let u:IUser = { //强力约束u这个字面量对象必须按照接口标准实现   
  name:'hrt',   
  age:18,
}
//那和type User 有什么区别呢?目前区别不大,在约束类中区别就大了。
//在绝大部分场景下,约束对象尽量用接口约束,而尽量不要使用type。
//接口和类型别名一样,不出现在编译结果中。

  • 接口约束函数

//接口约束
interface ICallBack {       
  (n:number): boolean
}
//类型别名可以实现同样的效果
type ICallBack = (n:number) => boolean 
//定界符号,具体的约束内容
type ICallBack = {       
  (n:number): boolean
}
function sum(numbers:number[], callBack:ICallBack):number {  
  let s:number = 0  
  numbers.forEach((d) => {      
    if(callBack(d)){       
       s += d       
    }   
  })    
  return s
}
let addSum:number = sum([1,2,3,4,5,6], (n) => n % 2 !== 0)
console.log(addSum)

既然类型别名和接口都能实现同样的效果,那类型别名和接口有什么区别呢?

接口是可以继承的, 可以通过继承接口来组合约束。类型别名不行了。

interface A {  
  T1:string,
}
interface B {  
  T2:string,
}
interface C extends A, B {   
  T3:boolean
}
// 类型别名实现同样的效果:需要通过 & 符号实现,是交叉类型。
type A = {  
  T1:string,
}
type B = { 
  T2:string,
}
type C = {   
  T3:boolean
} & A & B //C是交叉类型,交叉A, B

但是两者是可以混用的,同时,在书写的时候,类型别名是不能重复定义的,而接口是可以自动聚合的;

interface A {  
  T1:string,  
}  
interface B {    
  T2:string,  
}  
interface C extends A, B {    
  T3:boolean 
}  
// 类型别名和接口混用  
type D = {}  type test1 = D & C  
// 接口和类型别名混用  
// 接口只能扩展对象类型 或 对象类型与静态已知成员的交集。  
// 就是说 type D = '1' 是一个字面量这种非可继承的属性是会报错的 
interface test2 extends D { }

总结一下接口和类型别名的区别

  • 接口可以继承,类型别名不可以,类型别名可以通过 & 交叉类型实现同样的效果
  • 类型别名和接口是可以混用的,但是要注意接口只能继承扩展对象类型
  • 在OOP面向对象编程中,接口还可以被对象所实现 implements,类型别名不行
  • 在接口中,子接口不能覆盖父接口的约束成员。
  • 类型别名交叉的时候,相同的成员会交叉约束类型。两个成员的约束类型都会有,不覆盖。 

六、TS的类型兼容

什么是类型兼容呢?

比如两个类型,如果能完成赋值,A = B,则 B和A兼容;

TS的类型兼容用的是子结构辩型法。(也叫鸭子辩型法)

目标类型(A)约束有某些特征,赋值的类型(B)只要满足该特征即可,可以有多于的属于自己的;我只要满足你的所有特征就可以赋值了,这就叫鸭子辩型法。

我们来具体了解下TS对类型的判断

  1. 基本类型,要求完全匹配
  2. 对象类型,鸭子辩型法

interface Duck {    
  sound:"嘎嘎嘎"    
  swin:() => void
}
let person = {    
  name:'伪装成鸭子的人',   
  age:18,    
  // 类型断言,sound:"嘎嘎嘎" 这样的写的话TS会自动推断出sound的类型为string。   
  // 用了类型断言就是"嘎嘎嘎"类型的"嘎嘎嘎"值了;其实就是更换类型。   
  // 类型断言也可以在前面加<Type>来使用, <"嘎嘎嘎">"嘎嘎嘎"    
  sound:"嘎嘎嘎" as "嘎嘎嘎",    
  swin:() => {       
    console.log(this.name)  
  }
}
// 这就是鸭子辨型法,可以完成赋值
let duck:Duck = person
// 但是,不能直接把person的字面量对象直接写过来。
// 如果直接写过来,就更加严格了,只能是个鸭子
let duck2:Duck = {    
  sound:"嘎嘎嘎",    
  swin:() => {     
    console.log(this.name)  
  },    
  age:18,                //报错,不能将 age 分配给 Duck   
  name:'伪装成鸭子的人', //报错,不能将 name 分配给 Duck
}

  • 函数类型,一切无比的自然,但是返回值如果在约束的时候要求返回就一定要返回

//函数参数的鸭子辨型法   
interface ICallBack {    
   (n:number, i:number): boolean 
}    
function sum(numbers:number[], callBack:ICallBack):number{ 
  let s:number = 0      
  numbers.forEach((d, i) => {   
    // 注意这里接口约束要传两个,不能少     
    if(callBack(d, i)){        
      s += d        
    }     
  })      
  return s   
 }                         
//注意这里实际调用的时候根本没传完喔!     
let addSum:number = sum([1,2,3,4,5,6], (n) => n % 2 !== 0)  
// js的高阶函数forEach, map, filter都是这样的自然

七、readonly修饰符,修饰的目标是只读的

穿插一个修饰符;注意,只读修饰符不参与编译;
  • 普通修饰,可以用在接口、类型别名、类等

type A = {    
  readonly T1:string,  
}
interface C1 extends A1, B1 {   
  readonly T3:boolean
}

  • 比较需要注意的修饰细节

let arr: readonly number[] = [1,2,3]  
// 注意上面的修饰符号只是修饰类型。通过  
arr = [4,5,6]   
// 凡是涉及改变数组的相关函数/成员的提示就没了,只读; 比如 push 加入一项  
arr.push()  // 报错,不存在属性  
arr[0] = 3  // 这样都不行了。只读   
// 如果 const arr: readonly number[] = [1,2,3]  
// 相当于:cosnt arr: ReadonlyArray<number> = [1,2,3]  
// 注意,如果是修饰成员,写在成员的前面,相当于const。

八、类

这里我想分两部分讲,因为TS增加可很多类型,语法,对面向对象的编程支持更加的完善,所以将以基础部分和面向对象来讨论一下。先以类的基础部分讨论

  • tsconfig.json 属性的初始化检查,"strictPropertyInitialization": true 。更加严格的属性检查,检查初始化;检查构造函数中有没有赋值,或者属性列表有没有初始化默认值
  • 属性可以修饰为可选的,用问号?
  • 有些属性初始化之后就不能改变了, 用 readonly 修饰
  • 有些属性是不希望外部能读取的,使用访问修饰符。可以控制类中的某个成员的访问权限;可以在构造函数的形参增加一个访问修饰符,可以这样简写。达到传参赋初值效果(不做任何操作的初始化赋值才可以)

// "strictPropertyInitialization": true  更加严格的属性检查,检查属性初始化
// public 默认是这个访问权限,公开的,所有都可以访问
// private 私有的,只有在类中使用
// protected 受保护的,暂时不考虑
class name {    
  // 属性列表    
  readonly id:number;              //只读, 相当于const    
  gender:'男' | '女' = '男'    
  public pid?: string    
  private publishNumber:number = 3 //每天一共可以发多少篇文章    
  private curNumber:number = 0     //当前可以发布的文章数量    
  // 可以在形参中加修饰符达到初始化的效果。简化繁琐代码    
  constructor(public name:string, public age:number) {       
     this.id = Math.random()   
  }   
  public publish(title:string){   
     if(this.publishNumber > this.curNumber){  
        console.log('发布了一篇文章', title) 
         this.curNumber++  
     }else{        
         console.log('不能发布文章了')  
     }   
  }
}
const user = new name('hrt', 18)
user.publish('文章1')
user.publish('文章2')
user.publish('文章3')

  • 访问器,用于控制属性的读取和赋值

class name {       
  // 可以在形参中加修饰符达到初始化的效果。简化繁琐代码        
  constructor(public name:string, private _age:number) {      
      this.id = Math.random()      
  }        
  // 利用private控制,getAge() 是java的做法  
  // C# 是这样做的 get age,用的时候就像普通对象的获取 
  // ES6 的时候,也是这样玩的,其实这样也相当于是readonly   
  // C# 规范,写私有属性的时候尽量前面加下划线       
  get age(){         
      return this._age     
  }     
  set age(age: number){    
      // 类的属性,要受一定的限制控制   
      if(age >= 18 ){        
         return;        
      }         
      this._age = age  
  }  
}   
const user = new name('hrt', 18)  
user.publish('文章1') 
user.publish('文章2')  
user.publish('文章3') 
user.age = 10

OOP 部分后面再讲,还需要讲泛型,泛型是比较绕,和难理解的;

九、泛型

泛型是一个比较重要,实际项目运用也比较频繁的一个类型,需要掌握其使用;

什么是泛型?为什么需要泛型?

泛型:是指附属于函数、类、接口、类型别名之上的类型

为什么需要泛型:如果,实际项目编写中,我们需要一些丢失的信息,就需要泛型了;

第二点你可能很难以理解,实际上它相当于灵活需要一些类型信息,当时这些类型信息要在运行的时候才知道;举个例子。

// T表示泛型,依附于这个函数,泛型就相当于类型变量
function take<T>(arr:T[], n:number): T[]{    
  if(n >= arr.length){ 
     return arr   
  }    
  const res: T[] = []  
  for (let i = 0; i < n; i++) {  
     const element = arr[i];  
     res.push(element)  
  }    
  return res
}
// 调用函数的时候才知道什么类型
// 这样就能把丢失的信息(类型)找回来了
take<number>([1, '2', 3], 2) 
//指定类型的话,里面有字符串类型,就会报错;

  • 在函数名之后写上两个尖括号加泛型名称
  • 泛型就相当于类型变量,一般用T表示,定义的时候不知道什么类型,运行的时候才能确定是什么类型
  • 执行语句如果不使用指定泛型的类型,take([1, '2', 3], 2),就不会报错了,返回结果推导为 string | number; 因为TS可以类型推导,很多时候,TS可以智能的推导出类型,前提是使用了泛型。
  • 如果无法完成推导,并且有没有传递具体的类型,默认为空对象(相当于any)
  • 定义时候泛型可以使用默认值,不指定就是默认值。如:take<T = number>
  • 泛型是一个地方确定类型了,就可以推导确定出具体的类型了,很灵活强大;

泛型实际运用中的一些注意事项:

  • 泛型约束:函数使用泛型,参数是泛型,返回值也是泛型<T>, 对T进行一些约束。<T extends XXX>, 这样子一约束之后,到时候传过来的类型,必须满足XXX,鸭子辨型法。千万要注意,这里的 extends 不是继承的意思,是约束
  • 泛型对于前端同学来说确实是一个比较抽象的学习成本,特别是没有接触过强类型语言的;这里我比较推荐大家多查多练多看;这样你才能掌握泛型;有基础了直接看TS内置类型,antd 4.0 UI 库,antd 这个库使用的泛型相当多,也比较经典使用。

十、模块化

前端领域的模块化标准:ES6,commonJS/amd/cmd/umd/system/esnext;前两个是讨论的重点。
  • TS中如何书写模块化语句
  1. TS中,导入和导出,统一使用es6的模块化标准;
  2. 尽量不要使用默认 export default 导出,因为没有智能提示 ;应该用export。
  3. 导入的时候不要添加后缀名ts
  • 编译结果中的模块化
  1. 如果编译标准是ES6,没有区别的;如果编译结果的模块化标准是 commonJS,导出的声明会变成exports的属性

总结前面的知识点

  1. 基本类型:boolean number string object Array void never null undefined
  2. 字面量类型:具体的对象、字符等,或者元组
  3. 扩展类型:类型别名,枚举,接口,类。(实际上还有很多高级的联合类型)
  4. 类型别名和接口不产生编译结果,枚举和类产生编译结果。(枚举产生的就是类,类没啥区别)
  5. TS类:访问修饰符,readonly, 一些访问修饰符(public等)
  6. 泛型:解决某个功能和类型的耦合。(就是抽出一个通用的方法,方便代码重用、灵活)
  7. 类型兼容性:鸭子辨型法,子结构辨型法。(A如果想赋值给B,A必须满足B的结构,A的属性可以多不可以少)
  8. 类型兼容性,在函数类型兼容的时候,参数是可以少的,但是不可以多。要求返回必须返回,不要求你随缘
  9. 类型断言:开发者非常清楚某个类型,但是TS分辨不出来,可以用类型断言 as
  10. TS有很多的内置关键类型,像ReturnType<T>之类的,用到时候可以查文档;这个一个初学者的难点,多练多查多看;
  11. TS 还有 keyof、is 、in、typeof 之类的关键字,用到时候也可以多查阅文档,后面再介绍

十一、深入理解类和接口

这里我想再以面向对象的方式去深入一点理解接口和类,什么是面向对象?

面向对象:以类为切入点进行编程,提取出对象的抽象模版

这样说可能还是有很多小伙伴会懵圈了,特别是没有接触过强类型语言 java/c# 的一些面向对象编程方式的小伙伴;面向对象(Oriented Object Programing)只是一个编程的思维方法方式。

举个实例:人

  • 特征(属性):眼睛、鼻子、嘴巴、四肢、性别等
  • 动作(方法):说话、运动、制造工具

       以人这个对象为出发点,封装一个class对象模版,他拥有一些属性和方法;这就是面向对象开发方式;感兴趣的小伙伴也可以去了解下面向对象三大基础。封装、继承、多态;

react:类组件,可以产生对象的模版

如何学习OOP思想?我觉得最好的学习方式就是开发小游戏,很锻炼思维;

1. 里氏替换原则

类型匹配:TS 用的是鸭子辨型法。子类的对象,始终可以赋值给父类;在面向对象中,这种现象叫做里氏替换原则。如果要判断具体的子类型,用 instanceof 即可;

2. 继承的作用

继承可以描述类与类之间的关系,如A和B

  • B是父类,A是子类
  • B派生A,A继承自B
  • instanceof 判断具体是哪个子类型
  • B是A的基类,A是B的派生类
  • A继承自B,会拥有B的所有方法和属性(类访问控制符会影响)
  • 子类的对象,始终可以赋值给父类。(鸭子辨型法)

- 实例:
- let a:B = new A()
- a变量中的 this 指向还是正常的,还是执行A的实例
- 此时a只能使用子类和父类共有的方法,因为你声明的是B父类;
- 就是A子类有多余的都不能使用,一般不会这么奇葩写

3. 成员的重写(override)

  • 子类中覆盖父类的成员,这就叫重写
  • 重写不能改变父类成员的类型(属性)
  • 重写方法的时候,子类参数要和父类一样,返回值可以不一样,但差别太大就没必要使用同一个方法
  • 重写方法的时候,要注意this指向
  • super:在子类的方法中,可以使用super读取父类的成员。super 和 this 是有区别的。

4. 再谈类访问修饰符

  • 编译结果没有访问修饰符
  • readonly:只读修饰符
  • protected:受保护成员,只能在自身或者子类使用
  • private:私有的,只能自己使用
  • public:公共的,默认的
5. 继承的单根性和传递性
  • 单根性:每个类最多只能拥有一个父类,即extends 只能继承一个父类
  • 传递性:如果A(爷爷)是B(爸爸)的父类(有血缘关系),并且B(爸爸)也是C(儿子)的父类(有血缘关系),则A(爷爷)也是C(孙子)的父类(有血缘关系)。

6. 抽象类

为什么需要抽象类?
以中国象棋为例子,游戏里面存在各种棋子,棋子对象是抽象的,兵、马、炮这样的才是具体的实例。js 无法描述抽象类,ts可以;

语法:abstract class Chess { }

  • 有时某个类只是表示抽象的概念,用于提取子类的公有成员,而不能直接创建它的实例对象。
  • 该类可以作为抽象类,主要是为了解决代码重复的问题。不用兵、马、炮都写一遍类模版
  • 抽象类只能用于继承,不能 new 创建实例。抽象类是一个强约束,用于对类的强约束。
  • 如果抽象类中某个成员必须要子类实现,也可以使用abstract来进行强约束,要求子类一定要实现。
  • 抽象成员必须出现在抽象类中。
  • 抽象类可以再次继承抽象类,抽象类中可以先实现抽象成员,也可以到具体的子类再实现。
  • 这样的强约束,对于维护和团队合作是非常好的。

7. 静态成员 static

  • 属于某个构造函数的成员, 不附属在实例上,就要加上static关键字
  • 静态方法中的this, 执向的是当前类

8. 再谈接口

  • 接口用于约束类,对象、函数,是一个类型契约。
  • 在类的实例中,方法没有强约束力。容易将类型和方法耦合在一起。例如:A, B 继承自C。A,B有各自的方法,在某处要一起调用,就需要instance判断类型了。
  • 系统缺失对能力(方法)的定义 - 解决办法实现接口(implements 接口)
  • 面向对象领域中的接口的语义:表达了某个类是否拥有某种能力(方法)
  • 某个类具有某种能力,其实,就是实现了某种接口。implements interface(可以实现多个接口)
  • 如果implements实现了某种能力,必须要实现,不实现会报错。

interface IDown{      
   down():void  
}    
class Children extends Parent implements IDown{    
   down(): void {    
        throw new Error("Method not implemented.");     
   }   
}

9. 类型保护函数,来看个有趣的例子,判断某个变量是不是接口

// 相当于 a instanceof IDown, java中可以这么用TS不行。
function isHasIDown(chi: object): chi is IDown {  
  if((chi as unknown as IDown).方法){ 
     return true 
  }else{  
     return false
  }
}

10. 接口和类型别名的区别

  • 接口可以被类实现,类型别名不行
  • 接口可以继承类,表示该类的所有成员都在接口中

    class A {  
       name: string = 'A';  
    }    
    class B { 
       age: number = 1;  
     }    
    interface C extends A, B{ }   
    let c:C = {      
      name:'C',      
      age: 10  
    }

十二、索引器

使用语法: [ prop:string ]: string
索引器要写在最前面
可以动态的增加成员

class A {    
   [prop:string]:string  
   name:string = '1' 
} 
let c:A = new A()  
c.name = "ss" 
c.x = '2222' //动态增加成员

十三、类this指向问题

  • TS类中其实是使用了严格模式的,下面这样用会导致 this 为 undefined

class A {    
  name:string = 'hrt'  
  say(){       
     console.log(this.name)
  } 
}  
let c:A = new A() 
let s = c.say  s()

  • 而如果使用字面量的形式,this是any类型的

let c = {     
  name: 'hrt',   
  say(){     
     console.log(this.name)   
  }  
}  
let s = c.say  
s() //this是any类型的 

  • TS允许在书写函数的时候,手动声明该函数的this指向,将this作为第一个参数,只用于约束this,不是真正的参数,也不会出现在编译结果中。接口强约束了this指向,防止this乱指向,压根就犯不了错误。

interface IUser {   
  name:string,  
  age:number,   
  sayHello(this:IUser):void //强行约束this  
}  
let c:IUser = {   
   name: 'hrt',  
   age:18, 
   sayHello(){       
     console.log(this.name)  
   } 
}  
let s = c.sayHello 
s() //报错, this不匹配

十四、三个关键字

  • typeof,js中是得到一个数据的类型,ts中的 typeof 表示获取类型

// js中的 typeof  const a = '111'  typeof a -> string  
// ts 中的typeof,表示获取到的数据类型 
// b 的类型和 a 的类型保持一致,就用typeof 
let b:typeof a = "aaa" 

  • 当 typeof 作用于类的时候,得到的是该类的构造函数

 class User {}
 //1. 返回值要求的是 new User(),不是User
 function careteUser(cls:User): User {   
    //2. 如果传new User(),这里必须是一个构造函数
    return new cls()            
 }   
 //传类,参数不匹配,一定要满足这个奇葩需求怎么办呢?  
 const u = careteUser(User)  
 //那怎么办才能满足这奇葩需求呢?   
 //1. 构造函数约束  
 function careteUser(cls:new() => User)  
 //2. 或者这么干, typeof 作用于类的时候,得到的是该类的构造函数(是个类型) 
 function careteUser(cls:typeof User) 

  • keyof 作用于类、接口、类型别名,用于获取其它类型中的所有成员名,组成的联合类型;很强大实用的一个关键字

interface IUser {    
  pw:string,   
  age:number,   
  id:string    
} 
// 怎么约束prop呢? 它应该是某个具体类型的一个属性 
// keyof User 相当于 type prop = pw | age | id  
function printUserProperty(obj:User, prop:keyof User){  
  //obj[prop] 
}  
printUserProperty(user, "age")

  • in 该关键字往往和 keyof 联用,限制某个索引类型的取值范围

type Obj = {   
   //属性和值是字符串是string就满足条件    
   //怎么进一步约束取值范围呢?太广阔了     
   // [p:string]: string,   
   // in 操作符,in 一个联合类型就行了   
   [p in "pw" | "age" | "id"]: string  
}
const u:Obj = {  
   pw:' ',   
   age:'',   
   id:'',
} 
//Obj 不存在 abc 属性 
u.abc = "sss"
// 最佳写法 in keyof xxx 
// 得到一个新类型Obj,将IUser所有属性值类型变成了字符串 
type Obj = {   
  [p in keyof IUser]: string,
}

  • in 关键字还有很多玩法

// 完全重新塑造一个类型
type newObj = {     
   [p in keyof IUser]: IUser[p], 
} 
// 还可以这么玩,全设成只读的 
type newObj = {   
   readonly [p in keyof IUser]: IUser[p], 
} 
// 还可以这么玩,全设成可选的  
type newObj = {   
   [p in keyof IUser]?: IUser[p], 
}

十五、类型演算

类型演算其实就是根据已知类型关键字演算出新的类型,很实用

TS中预设了很多实用的类型演算,其实这些内置类型都是根据上面介绍的三个关键字来得出的

  • Partial<T>,将类型变成可选

type Partial<T> = { 
   [p in keyof T]?:T[p] 
}

  • ReadyOnly<T>, 只读

type ReadyOnly<T> = {
   readonly [p in keyof T]:T[p]
}

  • Required<T> 全部必选

type Required<T> = {    
   // -? 就是把问号去掉   
   [p in keyof T]-?: string, 
}

  • Pick<T, K> 选择,在T中选择K,K是T的子集

// K extends keyof T 这里是限制类型的意思,不是继承。
type Pick<T, K extends keyof T> = {   
   [P in K]: T[P]; 
}

还有很多内置的类型就不一一列举了,感兴趣的小伙伴可自行去读TS预设类型的源码,同时可以很快的提高你对泛型和高级类型的认识;

总结

       其实ts说难也不是很难,因为ts是一个静态可选的,学习成本曲线也不是很陡峭,除了类型系统,完全可以和 JS 一模一样;并且 TS 还增强了一些 OOP 特性;给JS套上一个拥有更多可能的壳;

       ts的东西远远不止这些,但是我们去如何学习掌握呢?我认为最好的方式就是动手去练;第一阶段先了解个大概语法,类型,一些关键字;跟着在线编译网站练;第二阶段就是直接去做写demo,小项目;这个阶段对你的提升绝对是很大的,中途你可能磕磕绊绊会遇到很多不懂的懵比的,但是只需要静下心来慢慢理解,查阅资料,很快你就可以熟练使用ts了;