TS基础掌握

268 阅读15分钟

TS

一.TypeScript

  1. TS是什么? => JS类型的超集

  2. 环境搭建?

     npm i typescript -g
    

    初始化项目

     npm init -y
     tsc --init
    

    新建 helloworld.ts 文件

     let url = 'nodeing.com'
    

    执行编译

     tsc helloworld.ts
    

二.类型系统

2.1.声明一个变量的类型举例:

 let url:string = "nodeing.com"
 let count:number = 20
 // 声明一个数组,里面元素类型为number,如果里面元素有别的类型会报错
 let arr:Array<number> = [1,2,3]
 // 声明数组的另一种写法
 let arr2: string[] = ["a", "b"]

#2.2.ts基础类型统计

以下这些关键字可以记一下

 boolean 布尔类型
 number  数字类型
 string  字符串类型
 Array   数组类型, 声明的时候也可以使用 sring[] 或者 number[]这种方式
 [string, number] 元组   let x: [string, number] = ["a", 2]
 enum    枚举  enum Color {Red, Green, Blue}
 any    任何类型都可以,跳过类型检查
 void   没有任何类型,和Any相反
 nullUndefined
 never  那些永不存在的值的类型,抛出异常或者死循环的函数永远没有返回值,类型就是never
 object 表示非原始类型, 除numberstringbooleansymbolnullundefined之外的类型。

img

#2.3.类型演示

  • 1.布尔类型
 let flag: boolean = true
 console.log(flag)
  • 2.数字类型

所有数字都是浮点数,不区分整数还是小数,可以支持二进制、八进制、十进制、十六进制

 let a: number = 0b1101
 let b: number = 0o1022
 let c: number = 10
 let d: number = 0xfff
 console.log(a, b, c, d)
  • 3.字符串类型

使用单引号和双引号来定义字符串

 let s1: string = 'hello,nodeing.com'
 let s2: string = "hello,nodeing.com"
 console.log(s1, s2)

使用反引号定义字符串

 let s1: string = 'nodeing.com'
 let s2: string = `hello, ${s1}`
 console.log(s2)
  • 4.数组类型

第一种定义数组的方式: 类型[]

 // number 规定了数组里面元素的类型
 let arr: number[] = [1, 2, 3]

第二种定义数组的方式: Array<类型>

 let arr:Array<number> = [1,2,3]
  • 5.元祖类型

元组( Tuple )表示一个已知数量和类型的数组,通俗的说,元组就是一个固定了的数组,长度不可以改变,但是类型可以是不同的

 let t1: [number, string] = [1, 'a']
 console.log(t1)
  • 6.枚举类型

枚举,你可以理解为列举,举个例子:请列举出一个星期包含的天数,此时,你可以从星期一数到星期日,枚举类型表示的就是这种可以列举出有穷元素的集,当一个变量有多种情况的时候,可以使用枚举类型,它可以让一组数值具有更让人理解的名字

 enum Gender {
   MALE,
   FEMALE
 }
 let g: Gender = Gender.MALE
 console.log(g) // 0

数字枚举

默认情况下,枚举元素的下标从0开始,当我们打印Gender.MALE的时候,输出的就是下标0,你也可以指定元素的数值

 enum Gender {
   MALE = 1,
   FAMALE
 }
 let g: Gender = Gender.MALE
 console.log(g)

字符串枚举

枚举的值可以是字符串

 enum gender {
   MALE = '男',
   FEMALE = '女'
 }
 console.log(gender.MALE, gender.FEMALE)

异构枚举

字符串和数字混在一起使用的情况就叫做异构枚举

 enum gender {
   MALE = 1,
   FEMALE = '女'
 }
 console.log(gender.MALE, gender.FEMALE)
  • 7.any类型

any表示可以使用任意类型,有时候我们不确定类型到底是什么的时候,就可以使用any类型,让检查器不对这些值进行检查直接通过编译阶段,举个例子:

 let obj:any = document.getElementById("root")

上面代码中,有时候能获取到元素,有时候获取不到元素,这个时候我们就可以使用any类型

说白了,any就是让你能选择性的移除类型检查,例如:定义一个具有多种类型元素的数组

 let arr2: any[] = [1, true, 'a']
  • 8.void类型 void和any是正好相反的,any表示的是你可以是任何类型,void表示的是没有任何类型,当一个函数没有返回值的时候,通常使用void类型
 function fn(): void {
   console.log(1)
 }
 ​
 fn()
  • 9.null和undefined

null和undefined这两种类型本身的用处不大,默认情况下,它们是所有类型的子类型,意味着你可以把null赋值给number或者其他类型

 let n1: number = null

需要注意的是,到底能不能把null赋值给其他类型,实际上和"strictNullChecks"这个配置有关系,你在tsconfig里面将"strictNullChecks"设置成false,才可以赋值,否则不能将null赋值给你别的类型

  • 10.never

never表示永远不的意思,那些永远不存在值的类型使用never表示,例如:一个函数中抛出了异常

 function fn(): never {
   throw new Error('报错了....')
 }

一个函数无法到达终点,例如,写出了死循环,也可以使用never

 function fn(): never {
   while (true) {
     console.log(1)
   }
 }

TIP

注意: void 可以被赋值为 null 和 undefined的类型。 never 则是一个不包含值的类型。 拥有 void 返回值类型的函数能正常运行。拥有 never 返回值类型的函数无法正常返回,无法终止,或会抛出异常

  • 11.object

object 表示非原始类型,也就是除 number,string,boolean,symbol,null或undefined 之外的类型

  • 12.联合类型

联合类型表示取值可以是多个类型中的一个,是或者关系

 let m: string | number
 m = "hello"
 m = 123
  • 13.类型断言

有些时候,你比编译器更清楚的知道当前的值是什么类型,这个时候你可以使用类型断言,通俗的理解就是,我敢断言某某某变量一定是某某某类型,举个例子:

 let m: string | number

上面定义了一个联合类型m,编译器认为你这里面存的可能是number,也可能是string,这个时候我清楚的知道它里面存的就是一个number,并且需要使用它的方法,我们可以这样写:

 let m: number | string
 ​
 console.log((m as number).toFixed(2))
 console.log((<number>m).toFixed(2))

这实际上就有点类似于类型转换,但是它不会具体转换成某种类型,只是在编译阶段起作用,并且在联合类型中你不能把类型断言成联合类型中没有的类型,例如上面的代码中只包含了number和string,你不能把它断言成boolean

let m: string | number
m = "hello"


console.log((m as string).toUpperCase())
console.log((<string>m).toUpperCase())
  • 14.字面量类型

通过值来定义一些类型

 type nType = 88 | "男" | "女"
 ​
 let n1: nType = 88
 let n2: nType = "男"
 let n3: nType = "女"
 // let n4: nType = 99  // 报错

TIP

注意: 字面量类型用来约束值只能是某几种值中的一个,联合类型约束的是几种类型中的一种

三.函数

函数定义

 // 函数声明  
 function showName(name: string): void {
   console.log(name)
 }
 // 函数调用
 showName("xiaoqiang")

函数表达式

 let showName2 = function(name: string, age: number): void {
   console.log(name, age)
 }
 showName2("小李", 20)

定义函数类型

 type show = (x: string, y: number) => string
 let showPerson: show = function(name: string, age: number) {
   return 'hello'
 }

可选参数

 // 使用 ? 表示参数是可选的
 function showName3(name: string, age?: number): void {
   console.log(name, age)
 }

默认参数

 // 直接给参数赋值,即为默认参数
 function showName4(name: string, age: number=10): void {
   console.log(name, age)
 }

剩余参数

 function sum(...arg: number[]): void {
   console.log(arg)
 }
 sum(1, 2, 3, 4)

函数重载

在ts中,函数重载体现为为一个函数定义多个类型定义

 function showName6(x:any) {
     if (typeof x == "string") {
         return "hello" + x;
     }
     if (typeof x == "number") {
         return 1 + x;
     }
 }
 console.log(showName6(2));
 console.log(showName6("xiaoli"));

ts中函数重载的写法

 function showName6(x: number): number
 function showName6(x: string): string
 function showName6(x: any): any {
   if (typeof x == 'string') {
     return 'hello' + x
   }
   if (typeof x == 'number') {
     return 1 + x
   }
 }
 console.log(showName6(111))
 console.log(showName6('xiaoli'))
 // console.log(showName6(true))  报错

四.类

4.1.类的定义

 class Person {
   name: string = ""
   showName(): void {
     console.log(this.name)
   }
 }
 let p = new Person()
 p.name = "xiaoqiang"
 p.showName()

#4.2.继承

 class Parent {
   surname: string = "li"
   name: string
   age: number
   constuctor(name: string, age: number) {
     this.name = name
     this.age = age
   }
   coding() {
     console.log("写代码")
   }
   showName() {
     console.log(this.name, this.age)
   }
 }
 class Child extends Parent {
   constructor(name: string, age: number) {
     super()
     this.name = name
     this.age = age
   }
   playBasketball() {
     console.log("打篮球")
   }
 }
 ​
 let c = new Child("xiaoli", 20)
 c.showName()

#4.3.成员修饰符

 public 公有属性, ts中成员默认是public,即可以自由访问的
 private 私有属性,类外部是不能访问这个属性的
 protected 受保护的属性,和private类似,外部不能访问,但是可以在子类中使用
 class Parent {
   surname: string = "li"
   private _name: string = "li lei"
   coding() {
     console.log("写代码")
   }
 }
 let p = new Parent()
 // 访问私有属性报错
 console.log(p._name)
 class Parent {
   protected surname: string = "li"
   private _name: string = "li lei"
   coding() {
     console.log("写代码")
   }
 }
 ​
 class Child extends Parent {
   playBasketball() {
     console.log("打篮球")
   }
   showName() {
     console.log(this.surname)
   }
 }
 ​
 let p = new Parent()
 let c = new Child()
 ​
 // console.log(p.surname)  外部不能访问
 // 子类里面可以使用 surname
 c.showName()

#4.4.存取器

let passcode = "secret passcode"

class Employee {
  private _fullName: string

  get fullName(): string {
    return this._fullName
  }

  set fullName(newName: string) {
    if (passcode && passcode == "secret passcode") {
      this._fullName = newName
    } else {
      console.log("Error: Unauthorized update of employee!")
    }
  }
}

let employee = new Employee()
employee.fullName = "Bob Smith"
if (employee.fullName) {
  console.log(employee.fullName)
}

#4.5.只读属性

一个属性只可以读取,不可以修改

 class Animal {
   readonly name: string = "xiaogou"
 }
 let dog = new Animal()
 // 不能设置  注意:这里只是类型检查的时候报错,但是编译后还是可以正常设置
 // dog.name = "xiaoqiang"
 console.log(dog.name)

#4.6.参数属性

如果只读属性需要在创建对象的时候传参来初始化,需要在构造函数里面进行

 class Animal {
   readonly name: string
   constructor(uname: string) {
     this.name = uname
   }
 }
 let dog = new Animal("xiaogou")
 ​
 console.log(dog.name)

参数属性可以帮助我们简化上面的代码,直接把只读属性放到构造函数的参数中即可

 class Animal {
   constructor(readonly name: string) {}
 }
 let dog = new Animal("xiaogou")
 ​
 console.log(dog.name)

#4.7.静态属性和静态方法

静态属性和静态方法就是不需要创建类的实例,可以直接调用的属性和方法

 class Person {
   static cname: string = 'xiaoqiang'
   static showName(): string {
     return this.cname
   }
 }
 console.log(Person.cname)
 console.log(Person.showName())

#4.8.抽象类

抽象是从众多的事物中抽取出共同的、本质性的特征,抽象类它首先是一个类,它和普通类的区别在于它提取了些共同的、本质的特征,可以用来定义一些标准,抽象类是不能被实例化的,通常它是作为派生类的基类在使用,这个和接口是不同的,等我们讲了接口以后再来对比差异

// 使用abstract定义抽象类
abstract class Person {}
// 这里报错,抽象类无法被实例化
let p = new Person()

抽象类定义了标准,需要在子类中去实现

abstract class Person {
  name: string
  abstract showName(): string
}

class Man extends Person {
  name = 'xiaoqiang'
  showName(): string {
    return this.name
  }
}

let p = new Man()
console.log(p.showName())

注意1:抽象类中的抽象方法也要使用 abstract关键字

注意2:抽象类的子类必须实现抽象类的抽象方法,不然会报错

 abstract class Person {
   name: string
   abstract showName(): string
 }
 ​
 class Man extends Person {
   name = 'xiaoqiang'
   
 }

注意3:抽象类中可以放普通方法

 abstract class Person {
   name: string
   abstract showName(): string
   showAge(): void {
     console.log(111)
   }
 }
 ​
 class Man extends Person {
   name = 'xiaoqiang'
   showName(): string {
     return this.name
   }
 }
 ​
 let p = new Man()
 console.log(p.showName())

五.接口

5.1.什么是接口

接口用于定义规范,为你的代码定义契约,和现实中的USB接口、Type-C接口等一致

#5.2.接口的应用场景

假如有一个函数,接收一个config对象,然后返回config里面的配置

 const getHostName = (config)=>config.hostname

如果直接这样写,config就是一个隐式的any类型,这在我们的配置里面是不允许的,因此,我们需要给这个config定义一个类型,但是这个config并不是我们的基本类型,所以我们需要使用接口来描述这个config的类型

我们定义一个config的接口

 interface Config {
   hostname: string
   port: number
 }
 const getHostName = (config: Config) => config.hostname

这样,我们就可以正常的调用函数了

 console.log(getHostName({ hostname: 'localhost', port: 8080 }))

如果不按照接口的规范来传参数,就会报错,例如:

 console.log(getHostName({ hostname: 'localhost', port: 8080, base: '/' }))  // 报错

#5.3.可选属性

有些时候,某些属性是可选的,比如port这个属性,假如不传就使用默认配置,这个时候,就需要将port这个属性设置成可选属性才行

 interface Config {
   hostname: string
   port?: number
 }
 const getHostName = (config: Config) => config.hostname
 ​
 console.log(getHostName({ hostname: 'localhost' }))

#5.4.只读属性

有些场景下,属性是不能修改的,比如root用户名,我们不允许修改,这个时候我们就要用到只读属性,我们的接口可以这样来描述

 interface Config {
   hostname: string
   port?: number
   readonly root: string
 }
 const getHostName = (config: Config) => (config.root = 'admin')
 ​
 console.log(getHostName({ hostname: 'localhost', root: 'root' }))

#5.5.函数类型

接口中是可以包含函数类型的,我们可以这样定义

 interface Config {
   hostname: string
   port?: number
   readonly root: string,
   callback: (name:string)=>string
 }
 const getHostName = (config: Config) => config.root
 ​
 console.log(getHostName({ hostname: 'localhost', root: 'root' ,callback: (name:string)=>name}))

#5.6.额外属性检查

传入的属性比接口定义的属性多的时候,会进行额外的属性检查,比如这种:

 interface Config {
   hostname: string
   port?: number
   readonly root: string
 }
 const getHostName = (config: Config) => config.root
 ​
 console.log(
   getHostName({
     hostname: 'localhost',
     root: 'root',
     callback: (name: string) => name
   })
 )

callback多传了,会报错

再比如,下面的代码:

 interface Config {
   hostname: string
   port?: number
   readonly root: string
 }
 const getHostName = (config: Config) => config.root
 ​
 console.log(
   getHostName({
     hostname: 'localhost',
     root: 'root',
     part: 9090
   })
 )

port 误操作写成了part,这个时候也会进行额外的类型检查,抛出错误,一般抛出错误的时候,你都要去查看当前到底是误操作还是本身就支持这些额外的属性,如果确定是支持的额外属性,你可以通过以下方式来解决报错问题:

第一,使用类型断言的方式

 interface Config {
   hostname: string
   port?: number
   readonly root: string
 }
 const getHostName = (config: Config) => config.root
 ​
 console.log(
   getHostName({
     hostname: 'localhost',
     root: 'root',
     part: 9090
   } as Config)
 )

第二种,添加字符串索引签名

 interface Config {
   hostname: string
   port?: number
   readonly root: string
   [propName: string]: any
 }
 const getHostName = (config: Config) => config.root
 ​
 console.log(
   getHostName({
     hostname: 'localhost',
     root: 'root',
     part: 9090
   })
 )

第三种,把对象字面量赋值给另一个参数

 interface Config {
   hostname: string
   port?: number
   readonly root: string
   [propName: string]: any
 }
 const getHostName = (config: Config) => config.root
 let conf = {
   hostname: 'localhost',
   root: 'root',
   part: 9090
 }
 console.log(getHostName(conf))

#5.7.可索引类型

假如说Config里面有一条配置,专门用来引入插件的,这些插件是一个集合,集合中到底用了哪些插件是不确定的,这个时候我们可以使用可索引类型来描述

 interface Plugins {
   [name: string]: string
 }
 interface Config {
   hostname: string
   port?: number
   readonly root: string
   [propName: string]: any
   plugins?: Plugins
 }
 const getHostName = (config: Config) => config.root
 let conf = {
   hostname: 'localhost',
   root: 'root',
   part: 9090
 }
 console.log(getHostName(conf))

#5.8.接口继承

假如有个基础的配置对象,在这个基础的配置对象上会增加一个新属性形成一个生产环境的配置,这个时候,我们生产环境的配置就可以继承基础配置

 interface baseConfig {
   hostname: string
   port?: number
   readonly root: string
 }
 interface Plugins {
   [name: string]: string
 }
 interface Config extends baseConfig {
   env: string
   plugins?: Plugins
 }
 const getHostName = (config: Config) => config.root
 let conf = {
   hostname: 'localhost',
   root: 'root',
   part: 9090,
   env: 'dev'
 }
 console.log(getHostName(conf))

六.泛型

6.1.什么是泛型

泛型从字面上理解,关键字就是这个 泛,广泛的意思,结合起来就是广泛的类型,怎样才能表示广泛的类型呢,方法很简单,搞一个变量来存储类型就行了,所以泛型在TypeScript中实际上就是一个类型变量,下面我们来看它的应用场景

假设有一个函数,传入一个数字,原封不动的返回一个数字,函数可以写成这样

 let fn = (num: number): number => num

如果把需求改成,传入一个字符串,返回的就是一个字符串,函数变成这样:

 let fn = (str: string): string => str

以此类推,如果我们想让它实现传入的是什么类型的值,就返回什么类型的值,这个时候函数应该怎么写呢?

 let fn = <T>(str: T): T => str
 console.log(fn(1))
 console.log(fn('1'))
 console.log(fn(true))

上面代码中的 T 就是类型变量,总结,定义:泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性

#6.2.多个类型参数

可以一次定义多个类型参数

 let fn = <T, U>(tuple: [T, U]): [T, U] => {
   return [tuple[0], tuple[1]]
 }
 console.log(fn([1, '999']))

#6.3.泛型变量的使用

 let fn = <T>(arg: T): void => {
   console.log(arg)
 }

上面我们定义了一个泛型变量T和参数arg,这个时候T的类型是由arg传入值决定的,如果我们要操作arg参数,例如:取arg的长度

 let fn = <T>(arg: T): void => {
   console.log(arg.length)
 }

这个时候编译会报错,因为arg是可以传入任意类型的,编译器并不知道arg到底是什么类型,假如:arg是一个数字,那么它身上就没有length这个属性,因此,这样使用是会有问题的,如果我们一定要取arg的长度,这个时候,我们可以将arg的类型定义成数组,写成T[]

 let fn = <T>(arg: T[]): void => {
   console.log(arg.length)
 }

把arg设置成类型T的数组,这样我们就把T作为了数组类型的一部分使用,也是比较灵活的,你传入的是数字类型的数组,T就是数字类型,传入的是字符串类型的数组,T就是字符串类型

泛型变量使用注意:

TIP

不是所有类型都能做的操作不能做,不是所有类型都能调用的方法不能调用

#6.3.泛型函数类型

我们可以先定义一个泛型函数,然后把这个函数作为类型使用,这就是泛型函数类型

 type Fn = <T>(arg: T) => T
 ​
 let fn: Fn = arg => {
   console.log(arg)
   return arg
 }

简写:

 let fn: <T>(arg: T) => T = arg => arg

#6.4.泛型接口

定义一个泛型接口

 interface User {
   <T>(uname: T): T
 }
 let user: User = uname => uname
 console.log(user('xiaohong'))

我们可以把接口里面的泛型变量拿出来,作为接口的参数传入

 interface User<T> {
   (uname: T): T
 }
 let user: User<string> = uname => uname
 console.log(user('xiaohong'))

这样做的好处就是我们可以在使用接口的时候传参数,比较灵活,其他的成员也可以使用这个参数类型

#6.5.泛型类

普通类

 class Person {
   constructor(readonly cname: string) {}
   showname: (uname: string) => string = uname => {
     return uname
   }
 }
 ​
 let p = new Person('xiaoli')
 console.log(p.cname)
 console.log(p.showname('xiaohei'))

泛型类:

 class Person<T> {
   constructor(readonly cname: T) {}
   showname: (uname: T) => T = uname => {
     return uname
   }
 }
 ​
 let p = new Person('xiaoli')
 console.log(p.cname)
 console.log(p.showname('xiaohei'))

#6.6.泛型约束

泛型可以代表任意类型,这个范围比较广,但是如果我们明确知道了传入的类型就是number或者string中的一种,就需要对泛型进行约束,说白了就是缩小范围

使用extends关键字对泛型进行约束

 type Params = string | number
 let fn = <T extends Params>(arg: T): T => {
   return arg
 }
 ​
 fn('1')
 fn(1)
 fn(true) // 报错
 interface Params {
   length: number
 }
 let fn = <T extends Params>(arg: T): void => {
   console.log(arg.length)
 }
 // keyof 是一个索引类型查询器,举例:
 interface Person {
   username: string
   age: number
 }
 type k = keyof Person
 ​
 //上面代码只用于说明keyof的用法
 ​
 ​
 function getProperty<T, K extends keyof T>(obj: T, key: K) {
   return obj[key]
 }
 ​
 let obj = { a: 1, b: '2', c: 3 }
 getProperty(obj, 'a')
 getProperty(obj, 'b')
 getProperty(obj, 'x') // 报错