typeScript学习笔记

初识TypeScript

TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持,它由 Microsoft 开发,代码开源于 GitHub上

安装TypeScript

命令行运行如下命令,全局安装TypeScript:

npm install -g typescript

  1. 安装完成后,在控制台运行如下命令,检查安装是否成功:

tsc -V

第一个TypeScript程序

  • 编写TS程序
//helloworld.ts
function greeter(person){
  return 'hello,'+person
}
let user='tao'
console.log(greeter(user))

手动编译代码

  1. 我们使用了.ts扩展名,但是这段代码仅仅是JS而已,在命令行上,运行 TypeScript 编译器:
    tsc helloworld.ts
    输出结果为一个helloworld.js文件,它包含了和输入文件中相同的JS代码,在命令行上,通过Node.js运行这段代码:
    node helloworld.js
    控制台输出:
    hello,tao

vscode自动编译

  1. 生成配置文件tsconfig.json
    tsc --init

  2. 修改tsconfig.json配置

"outDir":"./js",
"strict":false,

3.启动监视任务

  1. 终端->运行任务->监视tsconfig.json
  • 类型注解
    给函数参数添加:string类型的注解,如下:
function greeter(person:string){
   return 'hello,'+person
}
let user='tao'
console.log(greeter(user))
  1. TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。在这个例子里,我们希望函数接受一个字符串参数。然后尝试传入数组:
function greeter(person:string){
  return 'hello,'+person
}
let user=[0,1,2]
console.log(greeter(user))

重新编译,产生了一个错误:

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

接口

  1. 使用接口来描述一个拥有firstName和lastName字段的对象。在TypeScript里,只在两个类型内部的结构,那么这两个类型就是兼容的。这就允许我们在实现接口的时候只要保证包含了接口要求的结构就可以,而不必明确地使用implement语句
interface Person{
  firstName:string,
  lastName:string
}
function greeter(person:Person){
  return 'hello,'+person.firstName+person.lastName
}
let user={
 firstName:'tao',
 lastName:'xie'
}
console.log(greeter(user))

  1. 创建一个User类,它带有一个构造函数和一些公共字段。因为类的字段包含了接口所需要的字段,所以他们能很好的兼容
class User {
 fulName:string,
 firstName:string,
 lastName:string
 
 constructor(firstName:string,lastName:string){
    this.firstName = firstName
    this.lastName = lastName
    this.fullName = firstName + ' ' + lastName
  }
}

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = new User('tao', 'xie')

console.log(greeter(user))

TypeScript常用语法

  1. 基础类型
  • 布尔值
    最基本的数据类型就是简单的true/false,在JS和TS里叫做boolean
let isDone:boolean=false
isDone=true
//isDone=2 //error
  • 数字
    和JS一样,TS里的所有数字都是浮点数。这些浮点数的类型都是number。除了支持十进制和十六进制字面量,TS还支持ECMAScript2015中引入的二进制和八进制字面量
let a1:number=10 //十进制
let a2:number=0b1010 //二进制
let a3:number=0o12 //八进制
let a4:number=0xa //十六进制
  • 字符串
    使用string表示文本数据类型。和JS一样,可以使用双引号"或者单引号'表示字符串
let name:string='tom'
name='jack'
//name=12 //error
let age:number=12
const info=`My name is ${name},I am ${age} years old`
  • undefined和null
    默认情况下,null和undefined是所有类型的子类型。
let u:undefined=undefined
let u:null=null
  • 数组
//第一种定义数组:直接在元素类型后面加[]
let list1:number[]=[1,2,3]
//第二种定义数组
let list2:Array<number>=[1,2,3]
  • 元组Tuple
元组类型允许表示一个已知元素数量和类型的数组,各数组元素不必相同。
let t1:[string,number]
t1=['hello',1] //ok
t1=[1,'hello'] //error
当访问一个已知索引的元素,会得到正确的类型:

console.log(t1[0].substring(1)) //ok 
console.log(t1[1].substring(1)) //error number不存在substring方法
  • 枚举
enum使用枚举类型可以为一组数组赋予友好的名字
enum Color {
  Red,
  Green,
  Blue
}
//枚举数值默认从0开始依次递增
//根据特定的名称得到对应的枚举类型
let myColor:Color = Color.Green //0
console.log(myColor,Color.Red,Color.Blue)
默认情况下,从0开始为元素编号,也可以指定成员的数值:

enum Color {
  Red=1,
  Green,
  Blue
}
let c:Color=Color.Green

或者全部采用手动赋值:

enum Color {
  Red=1,
  Green=2,
  Blue=4
}
let c:Color=Color.Green
枚举类型提供一个便利是可以由枚举的值得到它的名字:

enum Color {
  Red=1,
  Green,
  Blue
}
let colorName:string=Color[2]
console.log(colorName) //Green
  • any
let notSure:any=4
notSure='maybe a string'
notSure=false
在对现有代码进行改写的时候,any类型是十分有用的,它允许在编译时可选择地包含或移除检查。并且当只知道一部分数据的类型时,any类型也是有用的:

let list:any[]=[1,true,'free']
list[1]=100
  • void
    某种程度上,void类型像是与any类型相反,它表示没有任何类型。当一个函数没有返回值时,通常会见到其返回值类型的是void:
/*表示没有任何类型,一般用来说明函数的返回值不能是undefined和null之外的值*/
function fn():void{
 console.log('fn()')
 //return undefined
 //return null
 //return 1  //error
}
声明一个void类型的变量没有什么用,因为你只能为它赋予undefined和null:

let unusable:void=undefined
  • object
    object表示非原始类型,也就是除number,string,boolean之外的类型
    使用object类型,就可以更好的表示像Object.create这样的API:
function fn2(obj:object){
  console.log('fn2()',obj)
  return {}
  // return undefined
  // return null
}
console.log(fn2(new String('abc')))
//console.log(fn2('abc')) //error
console.log(fn2(String))
  • 联合类型
    联合类型表示取值可以为多种类型中的一种
    需求1:定义一个一个函数得到一个数字或字符串值的字符串形式值
function toString2(x:number|string):string{
  return x.toString()
}

需求2:定义一个一个函数得到一个数字或字符串值的长度

function getLength(x:number|string){
  //return x.length //error
  if(x.length){ //error
    return x.length
  }else{
    return x.toString().length
  }
}

类型断言

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语法里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式。其一是 '尖括号’语法,另一个为as语法

/*
 类型断言:可以用来手动指定一个值的类型
 语法:
     方式一:<类型>值
     方式二:值 as 类型  tsx中只能用这种方式
*/

/*需求:定义一个函数得到一个字符串或者数值数据的长度*/
function getLength(x:number|string){
    if((<string>x).length){
      return (x as string).length
    }else{
      return x.toString().length
    }
}
console.log(getLength('abcd'),getLength(1234))

类型推断
类型推断:TS在没有明确的指定类型的时候推测出一个类型
有下面2种情况:1.定义变量时赋值,推断为对应的类型 2.定义变量时没有赋值,推断为any类型

/*定义变量时赋值,推断为对应的类型*/
let b9=123. //number
//b9='abc' //error

/*定义变量时没有赋值,推断为any类型*/
let b10 //any
b10=123
b10='abc'

接口

接口是对象的状态(属性)和行为(方法)的抽象(描述)

  • 接口初探
    需求:创建人的对象,需要对人的属性进行一定的约束
    id是number类型,必须有,只读
    name是string类型,必须有
    age是number类型,必须有
    sex是string类型,可以没有
    下面通过一个简单示例来观察接口是如何工作的:
/*
 在ts中,我们使用接口(interface)来定义对象的类型
 接口:是对象的状态(属性)和行为(方法)的抽象(描述)
 接口类型的对象
   多了或者少了属性是不允许的
   可选属性:?
   只读属性:readonly
*/

/*
需求:创建人的对象,需要对人的属性进行一定的约束
id是number类型,必须有,只读
name是string类型,必须有
age是number类型,必须有
sex是string类型,可以没有
*/

//定义人的接口
interface IPerson{
 id:number,
 name:string,
 age:number,
 sex:string
}
const person1:Person={
  id:1,
  name:'tom',
  age:20,
  sex:'男'
}

类型检查器会查看对象内部的属性是否与Iperson接口描述一致,如果不一致就会提示类型错误

  • 可选属性
    接口里的属性不全是必需的。有些是只在某些条件下存在,或者不存在
interface IPerson{
 id:number,
 name:string,
 age:number,
 sex?:string
}

带有可选属性的接口与普通接口定义差不多,只是在可选属性名字定义的后面加一个?号
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误

const person2:IPerson={
  id:1,
  name:'tom',
  age:20,
  //sex:'男' //可以没有
}
  • 只读属性
    一些对象属性只能在对象刚刚创建的时候修改其值。可以在属性名前用readonly来指定只读属性:
interface IPerson{
 readonly id:number,
 name:string,
 age:number,
 sex?:string
}

一旦赋值后再也不能被改变

const person2:IPerson={
 id:2,
 name:'tom',
 age:20,
 //sex:'男' //可以没有
 //xxx:12. //error 没有在接口中定义,不能有
}
person2.id=2 //error

type vs interface

type 可以而 interface 不行

type 可以声明基本类型别名,联合类型,元组等类型

// 基本类型别名
type Name = string

// 联合类型
interface Dog {
 wong();
}
interface Cat {
 miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

type 语句中还可以使用 typeof 获取实例的 类型进行赋值

// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div

其他骚操作

type StringOrNumber = string | number; 
type Text = string | { text: string }; 
type NameLookup = Dictionary<string, Person>; 
type Callback<T> = (data: T) => void; 
type Pair<T> = [T, T]; 
type Coordinates = Pair<number>; 
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

interface 可以 而 type不行

interface User {
 name: string
 age: number
}

interface User {
 sex: string
}

/*
User 接口为 {
 name: string
 age: number
 sex: string 
}
*/

一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。

readonly vs const

最简单判断该用readonly还是const的方法是看要把它作为变量使用还是作为一个属性。作为变量使用的话用const,作为属性使用的话用readonly

  • 函数类型
    接口能够描述JS中对象拥有的各种各样的外形。除了描述带有属性的普通对象外,接口也可以描述函数类型。
    为了使用接口表示函数类型,需要给接口定义一个调用签名。就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
/*
 接口可以描述函数类型(参数的类型与返回的类型)
*/
interface SearchFunc{
 (source:string,subString:string):boolean
}

这样定义后,可以像使用其他接口一样使用这个函数类型的接口。下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量:

const mySearch:SearchFunc=function(source:string,sub:string):boolean{
   return source.search(sub)>-1
}
console.log(mySearch('abcd','bc'))

类类型

  1. 类实现接口
/*
 类类型:实现接口
 1.一个类可以实现多个接口
 2.一个接口可以继承多个接口
*/
interface Alam{
  alert():any
}
interface Light{
  lightOn():void
  lightOff():void
}
class Car implements Alam {
 alert(){
  console.log('Car alert')
 }
}
一个类可以实现多个接口
class Car2 implements Alam,Light{
 alert(){
   console.log('Car alert')
 },
 lightOn(){
   console.log('Car light on')
 },
 lightOff(){
   console.log('Car light off')
 }
}
  1. 接口继承接口
    和类一样,接口也可以相互继承。能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重写的模块里
interface LightableAlarm extends Alam,Light{

}

基本示例:
/*
 类的基本定义与使用
*/
class Greeter{
 //声明属性
 message:string
 //构造方法
 constructor(message:string){
  this.message=message
 }
 //一般方法
 greet():string{
   return 'hello'+this.message
 }
}

//创建类的实例
const greeter=new Greeter('world')
//调用实例的方法
console.log(greeter.greete())

在引用任何一个类成员的时候都用了this。它表示访问的是类的成员。

后面一行,使用new构造了Greeter类的实例。它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。

最后一行通过greeter对象调用其greet方法

继承

基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

/*
 类的继承
*/
class Animal{
  run(distance:number){
    console.log(`Animal run ${distance}m`)
  }
}
class Dog extends Animal{
  cry()
   console.log('wang! wang!')
  }
}
const dog=new Dog()
dog.cry()
dog.run(100) //可以调用从父中继承得到的方法

这个例子展示了最基本的继承:类从基类中继承了属性和方法。这里,Dog是一个派生类,它派生自Animal基类,通过extends关键字。派生类通常被称作子类,基类通常被称作超类。
因为Dog继承了Animal的功能,因此我们可以创建一个Dog的实例,它能够cry()和run()

class Animal {
  name:string
  constructor (name;string){
    this.name=name
  }
  run(distance:number=0){
    console.log(`${this.name} run ${distance} m`)
  }
}

class Snake extends Animal {
  constructor(name:string){
    //调用父类型构造方法
    super(name)
  }
  //重写父类型的方法
  run(distance:number=5){
    console.log('sliding...')
    super.run(distance)
  }
}

class Horse extends Animal {
  constructor (name:string){
    //调用父类型构造方法
    super(name)
  }
  //重写父类型的方法
  run(distance:number=50){
    console.log('dashing...')
    //调用父类型的一般方法
    super.run(distance)
  }
  xxx(){
    console.log('xxx()')
  }
}

const snake=new Snake('sn')
snake.run()

const horse=new Horse('ho')
horse.run()

//父类型引用指向子类型的实例=》多态
const tom:Animal=new Horse('ho22')
tom.run()

/*如果子类型没有扩展的方法,可以让子类型引用指向父类型的类型*/
const tom3:Snake=new Animal('tom3')
tom3.run()
/*如果子类型有扩展的方法,不能让子类型引用指向父类型的实例*/
//const tom2:Horse=new Animal('tom2')
//tom3.run()

使用extends关键字创建了Animal的两个子类:Horse和Snake。派生类包含一个构造函数,它必须调用super(),它会执行基类的构造函数。而且,在构造函数里访问this的属性之前,一定要调用super()。这是TS强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。Snake类和Horse类都创建了run方法,它们重写了从Animal继承来的run方法,使得run方法根据不同的类而具有不同的功能。注意,即使tom被声明为Animal类型,但因为它的值是Horse,调用tom.run(34)时,它会调用Horse里重写的方法。

公共,私有与受保护的修饰符

  1. 默认为public
    可以自由的访问程序里定义的成员。在TS里,成员都默认为public。也可以明确的将一个成员标记为public。

  2. 理解private
    当成员被标记为private时,它就不能在声明它的类的外部访问。

  3. 理解protected
    protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。

/*
 访问修饰符:用来描述类内部的属性/方法的可访问性
 public:默认值,公开的外部可以访问
 private:只有类内部可以访问
 protected:类内部和子类可以访问
*/
class Animal {
  public name:string
  public constructor(name:string){
    this.name=name
  }
  public run(distance:name=0){
    console.log(`${this.name} run ${distance}m`)
  }
}
class Person extends Animal{
  private age:number=18
  protected sex:string='男'
  run (distance:number){
    console.log('Perosn jumping...')
    super.run(distance)
  }
}
class Student extends Person{
  run(distance:number=6){
    console.log('Studnet jumping...')
    
    console.log(this.sex) //子类能看到父类中受保护的成员
    //console.log(this.age) //子类看不到父类中私有的成员

    super.run(distance)
  }
}

console.log(new Person('abc').name)  //公开的可见
//console.log(new Person('abc').sex)  //受保护的不可见
//console.log(new Person('abc').age). //私有的不可见
readonly修饰符
可以使用readonly关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。
class Person{
  readonly name:string='abc'
  constructor(name:string){
   this.name=name
  }
}
let john=new Person('John')
//john.name='peter'  //error

参数属性

在上面的例子中,必须在Person类里定义一个只读成员name和一个参数为name的构造函数,并且立刻将name的值赋给this.name,这种情况经常会遇到。参数属性可以方便地让在一个地方定义并初始化一个成员。

class Person2{
  constructor (readonly name:string){
    
  }
}
const p=new Person2('jack')
console.log(p.name)

注意看是如何舍弃参数name,仅在构造函数里使用readonly name:string参数来创建和初始化name成员。把声明和赋值合并至一处。
参数属性通过给构造函数参数面前添加一个访问限定符来声明。使用private限定一个参数属性会声明并初始化一个私有成员;对于public和protected来说也是一样。

存取器

Typescript支持通过getters/setters来截取对对象成员的访问。它能帮助你有效的控制对对象成员的访问。

class Person {
  firstName:string='A'
  lastName:string='B'
  get fullName(value){
     return this.firstName+'-'+this.lastName
  }
  set fullName(value){
     const names=value.split('-')
     this.firstName=name[0]
     this.lastName=name[1]
  }
}

const p=new Person()
console.log(p.fullName)

p.firstName='C'
p.lastName='D'
console.log(p.fullName)

p.fullName='E-F'
console.log(p.firstName,p.lastName)

静态属性

可以创建类的静态成员,这些属性存在于类本身而不是类的实例上。

/*
静态属性:是类对象的属性
非静态属性:是类的实例对象的属性
*/
class Person{
  name1:string='A'
  static name2:string='B'
}
console.log(Person.name2)
console.log(new Person().name1)

抽象类

抽象类作为其他派生类的基类使用。它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节。abstract关键字是用于定义抽象类和在抽象类内部定义抽象的方法。

/*
抽象类
  不能创建实例对象,只有实现类才能创建实例
  可以包含未实现的抽象方法
*/
abstract class Animal{
  abstract cry()
  run(){
    console.log('run()')
  }
}
class Dog extends Animal{
  cry(){
    console.log('Dog cry()')
  }
}
const dog=new Dog()
dog.cry()
dog.run()

函数

  • 基本实例
    和JS一样,TS函数可以创建有名字的函数和匿名函数。你可以随意选择合适应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。
    下面的例子可以迅速回想起JS的两种函数:
//命名函数
function add(x,y){
  return x+y
}
//匿名函数
let myAdd=function(x,y){
  return x+y
}

函数类型

  1. 为函数定义类型
为上面函数添加类型
function add(x:number,y:number):number{
  return x+y
}

let myAdd=function(x:number,y:number):number{
  return x+y
}

可以给每个参数添加类型之后再为函数本身添加返回值类型。TS能够根据返回语句自动推断出返回值类型。

书写完整函数类型
let myAdd2:(x:number,y:number)=>number=function(x:number,y:number):number{ return x+ y }

可选参数和默认参数

TS里的每个函数参数都是必须的。这不是指不能传递null和undefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
JS里,每个参数都是可选的,可传可不传。没有传参的时候,它的值就是undefined。在TS里可以在参数名旁使用?实现可选参数的功能。

function bulidName(firstName:string='A',lastName?:string):string {
  if(lastName){
    return firstName+'-'+lastName
  } else {
    return firstName
  }
}

console.log(bulidName('C','D'))
console.log(bulidName('C'))
console.log(bulidName())

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。有时,想同时操作多个参数,或者并不知道会有多少参数传递进来。在JS里,可以使用arguments来访问所有传入的参数。
在TS里,可以把所有参数收集到一个变量里:剩余参数会被当做个数不限的可选参数。可以一个都没有,同样也可以有任意个。编译器创建参数数组,名字是你在省略号(...)后面给定的名字,可以在函数体内使用这个数组。

function info(x:string,...args:string[]){
  console.log(x,args)
}
info('abc','c','b','a')

函数重载

函数重载:函数名相同,而形参不同的多个函数

/*
函数重载:函数名相同,而形参不同的多个函数
需求:我们有一个add函数,它可以接收2个string类型的参数进行拼接,也可以接收2个number类型的参数进行想加
*/
//重载函数声明
function add(x:string,y:string):string
function add(x:number,y:number):number

//定义函数实现
function add(x:string | number,y:string | number):string | number {
  //在实现上要注意严格判断两个参数的类型是否相等,而不是简单的写一个x+y
  if(typeof x==='string'&&typeof y==='string'){
    return x+y
  }else if(typeof x==='number'&&typeof y==='number'){
    return x+y
  }
}

console.log(add(1,2))
console.log(add('a'+'b'))
//console.log(1,'a') //error

泛型

指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定具体类型的一种特性。

  • 引入
    下面创建一个函数,实现功能:根据指定的数量count和数据value,创建一个包含count个value的数组不用泛型的话,这个函数可能是下面这样:
function createArray(value:any,count:number):any[]{
  const arr:any[]=[]
  for(let index=0;index<count;index++){
    arr.push(value)
  }
  return arr
}

const arr1=createArray(11,3)
const arr2=createArray('aa',3)
console.log(arr1[0].toFixed(),arr2[0].split(''))

使用函数泛型

function createArray2<T>(value:T,count:number){
  const arr:Array<T>=[]
  for(let index=0;index<count;index++){
    arr.push(value)
  }
  return arr
}
const arr3=createArray2<number>(11,3)
console.log(arr3[0].toFixed())
// console.log(arr3[0].split('')) //error
const arr4=createArray2<string>('aa',3)
console.log(arr4[0].split(''))
//console.log(arr[4].toFixed()) //error
多个泛型参数的函数
//一个函数可以定义多个泛型参数
function swap<K,V>(a:K,b:V):[K,V]{
  return [a,b]
}
const result=swap<string,number>('abc',123)
console.log(result[0].length,result[1].toFixed())

泛型接口

在定义接口时,为接口中的属性或方法定义泛型类型
在使用接口时,再指定具体的泛型类型

interface ibaseCRUD<T>{
  data:T[]
  add:(t:T)=>void
  getById:(id:number)=>T
}

class User{
  id?:number;//id逐渐自增
  name:string;//姓名
  age:number;//年龄

  constructor(name,age){
    this.name=name
    this.age=age
  }
}

class UserCRUD implements IbaseCRUD <User>{
  data:User[]=[]
  add(user:User):void{
    user={...user,id:Date.now()}
    this.data.push(user)
    console.log('保存user',user.id)
  }
  getById(id:number):User{
    return this.data.find(item=>item.id===id)
  }
}

const userCRUD=new UserCRUD()
UserCRUD.add(new User('tom',12))
UserCRUD.add(new User('tom2',13))
console.log(userCRUD.data)

泛型类

在定义类时,为类中的属性或方法定义泛型类型 在创建类的实例时,再指定特定的泛型类型

class GenericNubmer<T>{
  zeroValue:T
  add:(x:T,y:T)=>T
}
let myGenericNumber=new GenericNubmer<number>()
myGenericNumber.zeroValue=0
myGenericNumber.add=function(x,y){
  return x+y
}

let myGenericString=new GenericNubmer<string>()
myGenericString.zeroValue='abc'
myGenericString.add=function(x,y){
  return x+y
}
console.log(myGenericString.add(myGenericNumber.zeroValue,'test'))
console.log(myGenericNumber.add(myGenericNumber.zeroValue,12))

泛型约束

直接对一个泛型参数取length属性会报错,因为这个泛型根本不知道它是否有这个属性

//没有泛型约束
function fn<T>(x:T):void{
  //console.log(x.length) //error
}

可以使用泛型约束来实现

interface Lengthwise{
  length:number
}
//指定泛型约束
function fn2<T extends Lengthwise>(x:T):void{
  console.log(x.length)
}

需要传入符合约束类型的值,必须包含length属性:

fn2('abc')
//fn2(123) //error number没有length属性

声明文件

当使用第三方库时,需要引用它的生命文件,才能获取对应的代码补全、接口提示等功能

什么是生命语句

假如想使用第三方库jQuery,一种常见的方式是在html中通过<script>标签引入jQuery,然后就可以使用全局变量$或jQuery

但是在ts中,编译器并不知道$或jQuery是什么东西

/*
 当使用第三方库时,需要引用它的生命文件,才能获得对应的代码补全、接口提示等功能。
 声明语句:如果需要ts对新的语法进行检查。需要加载了对应的类型说明代码
 declare var jQuery:(selector:string)=>any
 声明文件:把声明语句放到一个单独的文件(jQuery.d.ts)中,ts会自动解析到项目中所有声明文件
 下载声明文件:npm install @type/jquery --save-dev
*/
jQuery('#foo')
//ERROR:Cannot find name 'jQuery'
这时,需要使用declare var 来定义它的类型

declare var jQuery:(selector:string)=>any
jQuery('#foo')

一般声明文件都会单独写成一个xxx.d.ts文件

创建01_jQuery.d.ts,将声明语句定义其中,TS编译器会扫描并加载项目中所有的TS声明文件

declare var jQuery:(selector:string)=>any

很多的第三方库都定义了对应的声明文件库,库文件名一般为@type /xxx,可以在https://www.npmjs.com/package/package进行搜索

有的第三方库在下载时就会自动下载对应的声明文件库(比如:webpack),有的可能需要单独下载(比如jQuery/react)

内置对象

JS中有很多内置对象,它们可以直接在TS中当做定义好了的类型

内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指ECMAScript和其他环境(比如DOM)的标准

  1. ECMAScript的内置对象
/*1.ECMAScript的内置对象*/
let b:Boolean=new Boolean(true)
let n:Number=new Number(1)
let s:String=new String('abc')
let d:Date=new Date()
let r:RegExp=/^1/
let e:Error=new Error('error message')
b=true
// let bb:boolean=new Boolean(2) //error
  1. BOM和DOM的内置对象
const div:HTMLELement=document.getElementById('test')
const divs:NodeList=document.querySelectorAll('div')
document.addEventListener('click',(event:MouseEvent)=>{
   console.log(event.target)
})
const fragment:DocumentFragment=document.createDocumentFragment()

内置方法

Record

type petsGroup = 'dog' | 'cat' | 'fish';
interface IPetInfo {
    name:string,
    age:number,
}

type IPets = Record<petsGroup, IPetInfo>;

const animalsInfo:IPets = {
    dog:{
        name:'dogName',
        age:2
    },
    cat:{
        name:'catName',
        age:3
    },
    fish:{
        name:'fishName',
        age:5
    }
}

可以看到 IPets 类型是由 Record<petsGroup, IPetInfo>返回的。将petsGroup中的每个值(‘dog’ | ‘cat’ | ‘fish’)都转为 IPetInfo 类型。

当然也可以自己在第一个参数后追加额外的值,如下面:

type petsGroup = 'dog' | 'cat' | 'fish';
interface IPetInfo {
    name:string,
    age:number,
}

type IPets = Record<petsGroup | 'otherAnamial', IPetInfo>;

const animalsInfo:IPets = {
    dog:{
        name:'dogName',
        age:2
    },
    cat:{
        name:'catName',
        age:3
    },
    fish:{
        name:'fishName',
        age:5
    },
    otherAnamial:{
        name:'otherAnamialName',
        age:10
    }
}

可以看到在demo1的基础上,demo2在

type IPets = Record<petsGroup | ‘otherAnamial’, IPetInfo>; 中除了petsGroup的值之外,还追加了 'otherAnamial’这个值。

下面看一个略复杂的例子,用axios将http的几个请求封装一下,使用Record定义每个请求方法的形状。

enum IHttpMethods {
    GET = 'get',
    POST = 'post',
    DELETE = 'delete',
    PUT = 'put',
}

const methods = ["get", "post", "delete", "put"];

interface IHttpFn {
    <T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
}

type IHttp = Record<IHttpMethods, IHttpFn>;

const httpMethods: IHttp = methods.reduce((map: any, method: string) => {
    map[method] = (url: string, options: AxiosRequestConfig = {}) => {
        const { data, ...config } = options;
        return (axios as any)[method](url, data, config)
            .then((res: AxiosResponse) => {
                if (res.data.errCode) {
                    //todo somethins
                } else {
                    //todo somethins
                }
            });
    }
    return map
}, {})

export default httpMethods;

上面这个demo就先枚举除了几个常见的http请求的方法名,而每个方法都接受请求的url以及可选参数config,然后每个方法返回的都是一个Promise。这种业务常见使用Record再合适不过了。使用下面的方式定义了每个方法的形状。

type IHttp = Record<IHttpMethods, IHttpFn>;
最后只需要遍历一下几个方法,对每个方法有各自的具体实现即可。这里是用了reduce的特性,遍历了一下数据,然后将所有的方法体放在一个对象中,最终结果用 httpMethods接受,再将httpMethods对外暴露出去,那么外面就可直接调用了。这里把一些业务的部分抽离出去了(比如设置请求头、设置token之类的),只是为了简单说明一个比较合适使用Record的业务场景。

Uppercase

构建一个类型,定义其属性全是小写。然后使用Uppercase方法将其全部转化为大写。

type Role = "admin" | "user" | "guest";

// 不好的做法 💩
type UppercaseRole = "ADMIN" | "USER" | "GUEST";

// 正确的做法 ✅
type UppercaseRole = Uppercase<Role>; // "ADMIN" | "USER" | "GUEST"

Lowercase

与前面一个例子相反,先构建一个属性全是大写的类型,然后使用Lowercase方法将其全部转化为小写。

type Role = "ADMIN" | "USER" | "GUEST";

// 不好的做法 💩
type LowercaseRole = "admin" | "user" | "guest";

// 正确的做法 ✅
type LowercaseRole = Lowercase<Role>; // "admin" | "user" | "guest"

Capitalize

将所有属性的首字母大写。

type Role = "admin" | "user" | "guest";

// 不好的做法 💩
type CapitalizeRole = "Admin" | "User" | "Guest";

// 正确的做法 ✅
type CapitalizeRole = Capitalize<Role>; // "Admin" | "User" | "Guest"

Uncapitalize

与Capitalize相反,将所有属性取消首字母大写。

type Role = "Admin" | "User" | "Guest";

// 不好的做法 💩
type UncapitalizeRole = "admin" | "user" | "guest";

// 正确的做法 ✅
type UncapitalizeRole = Uncapitalize<Role>; // "admin" | "user" | "guest"

Partial

将定义的接口中的属性设置为可选属性。

interface User {
  name: string;
  age: number;
  password: string;
}

// 不好的做法 💩
interface PartialUser {
  name?: string;
  age?: number;
  password?: string;
}

// 正确的做法 ✅
type PartialUser = Partial<User>;

Required

与Partial相反,Required可以将接口中的可选属性设置为必需。

interface User {
  name?: string;
  age?: number;
  password?: string;
}

// 不好的做法 💩
interface RequiredUser {
  name: string;
  age: number;
  password: string;
}

// 正确的做法 ✅
type RequiredUser = Required<User>;

Readonly

将属性设置为只读

interface User {
  role: string;
}

// 不好的做法 💩
const user: User = { role: "ADMIN" };
user.role = "USER";

// 正确的做法 ✅
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { role: "ADMIN" };
user.role = "USER"; // 将属性'role'设置为只读后,再次对'role'设值就会报错

Pick
就是从一个复合类型中,取出几个想要的类型的组合,例如:

interface User {
  name: string;
  age: number;
  password: string;
}

// 不好的做法 💩
interface UserPartial {
  name: string;
  age: number;
}

// 正确的做法 ✅
type UserPartial = Pick<User, "name" | "age">;

Omit

Omit是TypeScript3.5新增的一个辅助类型,它的作用主要是:以一个类型为基础支持剔除某些属性,然后返回一个新类型。

interface User {
  name: string;
  age: number;
  password: string;
}

// 不好的做法 💩
interface UserPartial {
  name: string;
  age: number;
}

// 正确的做法 ✅
type UserPartial = Omit<User, "password">;

Exclude

将类型中其中一些属性排除,并创建排除属性后的新类型。

type Role = "ADMIN" | "USER" | "GUEST";

// 不好的做法 💩
type NonAdminRole = "USER" | "GUEST";

// 正确的做法 ✅
type NonAdmin = Exclude<Role, "ADMIN">; // "USER" | "GUEST"

Extract

它通过从可分配给联合的类型中提取所有联合成员来创建新类型。

type Role = "ADMIN" | "USER" | "GUEST";

// 不好的做法 💩
type AdminRole = "ADMIN";

// 正确的做法 ✅
type Admin = Extract<Role, "ADMIN">; // "ADMIN"

NonNullable

通过从类型中排除null和undefined来创建新类型。

type Role = "ADMIN" | "USER" | null;

// 不好的做法 💩
type NonNullableRole = "ADMIN" | "USER";

// 正确的做法 ✅
type NonNullableRole = NonNullable<Role>; // "ADMIN" | "USER"
posted @ 2022-03-20 13:35  ajajaz  阅读(89)  评论(1编辑  收藏  举报