TypeScript 入门

116 阅读13分钟

前言

一篇学习笔记,有问题欢迎指正。


TypeScript 认知

为什么需要学习TypeScript

  1. 获得更好的开发体验
  2. 解决js中一些难以处理的问题

JS开发中存在的问题

  1. 使用了不存在的变量、函数或成员
  2. 类型错误(把不确定的类型当确定的类型进行处理)
  3. ......

JS的原罪

js语言本身的特性,决定了该语言无法适应大型的复杂项目

  1. 弱类型语言:某个变量可以随时更换类型
  2. 解释性语言:错误发生的时间是在运行时

TS概述

  • TS是JS的超集,是一个可选的、静态的类型系统

    • 超集
    • 可选的:可用可不用。学习曲线非常平滑
    • 静态的:无论是浏览器环境还是node环境,无法直接识别ts代码,需要通过tsc(ts编译器)转换。类型检查发生的时间是在编译时
    • 类型系统:对代码中所有的标志符(变量、函数、参数、返回值)进行类型检查
  • 有了类型检查,无形中增强了面向对象开发

TS常识

  • 2012年微软发布
  • Anders Hejlsberg负责开发TS项目
  • 开源,拥抱ES标准

在node中搭建TS开发环境

1.新建一个空文件夹来学习ts

2.全局安装 typescript

npm install typescript -g

3.在项目下新建一个index.ts

let say:string = 'hello'

4.把ts文件编译为js文件,在控制台输入命令

tsc index.ts
// index.js
var say = 'hello'

这时候你会发现项目中出现一个同名的index.js文件,但是神奇的是index.ts文件出现了报错信息

tcs index.ts会有几种假设:

  • 假设当前的执行环境是浏览器环境
  • 如果代码中没有使用模块化语句,便认为代码是全局执行
  • 编译的目标代码是es3

我们可以看到编译出来的index.js文件中say变量是全局的,这时index.ts文件声明的say变量就报错了

这时候tsconfig.json配置文件就出场了

5.生成 tsconfig.json 配置文件

具体配置在官网查阅

tsc --init

使用了配置文件之后,使用tsc进行编译时不用跟上文件名,否则会忽略掉配置文件

类型约束

TS 是一个可选的静态的类型系统

如何进行类型约束

仅需要在 变量、函数的参数、函数的返回值 位置加上 :类型
ts在很多场景中可以完成类型推导
any: 表示任意类型,对该类型,ts不进行类型检查\

let name:string
let age  // any类型

function sum(a: number, b: number):number { return a + b }

// 类型推导
let say = 'hi'
say = 123   // 报错:不能将number类型赋值给string类型

基本类型

  • number:数字
  • string:字符串
  • boolean:布尔
  • 数组
    • number[] || Array<number>
    • string[] || Array<string>
    • any[]
    • ......
  • object:对象
  • null 和 undefined:是所有其他类型的子类型,它们可以赋值给其他类型

其他常用类型

  • 联合类型:多种类型任选其一
    • 类型保护:当对某个变量进行类型判断后,在判断的语句块中便可以确定它的确切类型,如typeof可以触发类型保护
let name: string | undefined
if(typeof name === "string"){
    // 类型保护, name在这里面必然是string类型
}
  • void类型:通常用于约束函数的返回值,表示该函数没有任何返回
  • never类型:通常用于约束函数的返回值,表示该函数永远不可能结束
function throwError(msg: string):never {
	  throw new Error(msg)
}


function alwaysDoSomething():never {
    while(true){}
}
  • 字面量类型:使用一个值进行约束
let name:"yq"  // name 只能为“yq”
let gender: "men" | "women"  // gender 只能赋值为“men”和“women”
let arr:[]  // arr永远只能取值为空数组
let user: {  // 表示user必须赋值为有string类型的name、number类型的age 的对象
  name:string
  age:number
}
  • 元组类型(Tuple):一个固定长度的数组,并且数组中每一项的类型确定
let arr:[string, number]  // arr 只能有两项,第一项为string类型,第二项为number类型
  • any类型:any类型可以绕过类型检查,因此,any类型的数据可以赋值给任意类型
let data:any = "hahaha"
let num:number = data  // 此处不会报错

函数的相关约束

  • 函数重载:在函数实现之前,对函数调用的多种情况进行声明

注意:函数重载真正执行的是同名函数最后定义的函数体。在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法,只能定义类型

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 s = combine(1, '2')  // 报错,没有与此调用匹配的重载。
  • 可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递,必须是最后一个参数
  • 默认参数:默认参数一定是可选参数
function sum(a:number, b:number, c?:number, d:number = 0) {}
  • 剩余参数:当你并不知道有多少个参数传进来,可通过把所有参数收集到一个变量里
function sum(a:number, ...restNum:number[]){}

扩展类型-类型别名

对已知的一些类型定义名称,不出现在编译结果中。
type 类型名称 = xxx

type Gender = 'men'|'women'
type User = {
  name:string
  age:number
  gender:Gender
}

function getUsers(g:Gender):User[] {
  return [{name:'yq', age:22, gender:g}]
}

扩展类型-枚举

枚举通常用于约束某个变量的取值范围,字面量和联合类型配合使用,也可以达到同样的目标\

字面量类型的问题

  • 在类型约束位置,会产生重复代码,但可以使用类型别名解决该问题
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,会产生大量的修改
  • 字面量类型不会进入到编译结果

定义枚举

枚举会出现在编译结果中,编译结果中表现为对象
enum 枚举名 {
枚举字段1 = 值1,
枚举字段2 = 值2,
......
}

枚举的规则:

  • 枚举的值可以是字符串或数字
  • 数字枚举的值会自动递增
  • 被数字枚举约束的变量,可以直接赋值为数字
  • 数字枚举的编译结果和字符串枚举的编译结果有差异
enum Gender {
  male = '男',
  female = '女'
}
  
enum Level {
  Level1 = 1,
  Level2,    // 自动识别为2
  Level3     // 自动识别为3
}
  
let l: Level = Level.Level2
l = 3    // 数字枚举可以直接赋值为数字,不会报错
// 编译结果
var Gender;
(function (Gender) {
    Gender["male"] = "\u7537";
    Gender["female"] = "\u5973";
})(Gender || (Gender = {}));

var Level;
(function (Level) {
    Level[Level["Level1"] = 1] = "Level1";
    Level[Level["Level2"] = 2] = "Level2";
    Level[Level["Level3"] = 3] = "Level3";
})(Level || (Level = {}));
// Level = {
//   '1': 'level1',
//   '2': 'level2',
//   '3': 'level3',
//   'level1': 1,
//   'level2': 2,
//   'level3': 3
// }

最佳实践:

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值

扩展-位枚举(枚举的位运算)

针对数字枚举
位运算:两个数字换算成二进制后进行的运算

enum Permission {
  Read   = 1,   // 0001
  Write  = 2,   // 0010
  Create = 4,   // 0100
  Delete = 8    // 1000
}

// 如何组合权限
// 使用位运算-或运算
let p: Permission = Permission.Read | Permission.Write  // 0011

// 如何判断是否拥有某个权限
// 使用位运算-且运算
function hasPermission(target: Permission, per: Permission){
  return (target & per) === per
}
hasPermission(p, Permission.Read)

// 如何删除某个权限
// 使用位运算-异或
p = p ^ Permission.Write

扩展类型-接口 interface\

用于约束类、对象、函数的契约(标准)
“接口”其实就相当于 “标准”
不会出现在编译结果中 契约(标准)的形式:

  • API文档,弱标准
  • 代码约束,强标准

接口约束

接口约束对象(与“类型别名”无大区别)

interface User {
  name: string
  age: number
}

type User = {
  name: string
  age: number
}

接口约束函数

// 函数作为对象的属性进行约束
interface User {
  say: () => void
  cb(): void
}

// 直接约束函数
interface Condition {
  (n: number): boolean
}
// type Condition = (n: number) => boolean  // 类型别名

function sum (numbers: number[], cb: (n: number) => boolean) {}
function sum (numbers: number[], cb: Condition) {
  let s = 0
  number.forEach(n => {
    if(cb(n)) {
      s += n
    }
  })
  return s
}
sum([3,4,5,6,7], n => n % 2 === 0)

接口继承

可以通过接口之间的继承,实现多种接口的组合

interface A {
  T1: string
}

interface B extends A {
  T2: number
}
// 继承A和B
interface C extends A, B {
  T3: boolean
}

let u:C = {
  T1: 'hahaha',
  T2: 28,
  T3: true
}

使用类型别名可以实现类似的组合效果,使用 "&" 交叉类型

type A = { t1: string }
type B = { t2: number }
type C = { t3: boolean } & A & B

区别:

  • 子接口不能覆盖父接口的成员(如不能在接口C中重写T1为number类型)
  • 交叉类型会把同样成员的类型进行交叉(如在类型别名C中重写T1为number类型,那么这时T1同时具备了number和string类型的方法,没法直接赋值为number或string类型的值了)

readonly

只读修饰符,修饰的目标是只读的
不在编译结果中

interface User {
  readonly id: string
  name: string
  readonly arr: number[]
  readonly list: readonly number[]
}

let u:User = {
  id: '001',
  name: 'yq',
  arr: [1,2],
  list: [1,2]
}

u.id = '002'      // 报错,无法分配到 "id" ,因为它是只读属性
u.arr = [1, 2, 3] // 报错,无法分配到 "arr" ,因为它是只读属性
u.arr.push(3, 4)
u.list.push(3)    // 报错,类型“readonly number[]”上不存在属性“push”

let arr: readonly number[] = [3,4,5]
arr.push(1)   // 报错,类型“readonly number[]”上不存在属性“push”
arr[0] = 1    // 报错,类型“readonly number[]”中的索引签名仅允许读取。

类型兼容

B->A,如果能完成赋值,则B和A类型兼容

  • 基本类型:完全匹配
  • 对象类型:鸭子辨型法
  • 函数类型:一切无比的自然
    • 参数:传递给目标函数的参数只能少不能多
    • 返回值:要求返回必须返回,不要求返回随意

鸭子辨型法(子结构辨型法)

目标类型需要某一些特征,赋值的类型只要能满足该特征即可
当直接使用对象字面量赋值的时候,会进行更加严格的判断

interface Duck {
  sound: '嘎嘎' | '嘎嘎嘎'  // 字面量类型
  swin(): void
}

let chick = {
  name: '披着鸭皮的鸡',
  age: 2,
  sound: '嘎嘎嘎' as '嘎嘎嘎',  // 类型断言
  swin() {
    console.log(this.name + "正在游泳,并发出了" + this.sound + "声")
  }
}

let duck: Duck = chick  // 后续name 和 age 是不能用的(ts用心良苦的点)
console.log(duck.age)   // 报错,类型“Duck”上不存在属性“age”

// 当直接使用对象字面量赋值的时候,会进行更加严格的判断
let d: Duck = {
  sound: '嘎嘎嘎',
  swin() {},
  name: 'yam'   // 报错,对象文字可以只指定已知属性,并且“name”不在类型“Duck”中
}

用心良苦的点(比如请求用户信息的接口给到你一大堆无用信息,你只需要个别属性)

interface User {
  name?: string
  age: number
}

let responseUserInfo = 请求接口()
let u: User = responseUserInfo

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。其实就是你需要手动告诉 ts 就按照你断言的那个类型通过编译(这一招很关键 有时候可以帮助你解决很多编译报错)

类型断言有两种形式:

这两种方式虽然没有任何区别,但尖括号格式会与 react 中 JSX 产生语法冲突,因此更推荐使用 as 语法

// 方式一:尖括号
let someVal: any = 'xxx'
let strLen: number = (<string>someVal).length

// 方式二:as
let someVal: any = 'xxx'
let strLen: number = (someVal as string).length

非空断言 在上下文中当类型检查器无法断定类型时 一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型

let flag: null | undefined | string
flag!.toString() // ok
flag.toString()  // error

扩展类型-类

面向对象思想
使用属性列表来描述类中的属性

属性的初始化检查

strictPropertyInitialization: true
属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

属性修饰

  • 可选 ?
  • 只读 readonly

访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:公开的。默认的访问修饰符,所有的代码均可以访问
  • private:私有的。只能在类中访问
  • protected

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写

class User{
  readonly id: number  // 属性修饰-只读
  name: string
  // age: number  属性简写
  gender: 'male'|'female' = 'male'  // 属性初始化-属性默认值
  pid?: string  // 属性修饰-可选
  
  private publishNum: number = 3
  private curNum: number = 0
  
  constructor(name: string, public age: number) {
    this.id = Math.random()
    this.name = name  // 属性初始化-构造函数中
    // this.age = age  属性简写
  }
  
  publish(title: string) {
    if(this.curNum < this.publishNum){
      this.curNum++
      console.log("发布一篇文章:" + title)
    } else {
      console.log("你今日发布文章数量已达到上限")
    }
  }
}

访问器

用于控制属性的读取和赋值

class User {
  name: string
  private _age: number
  
  constructor() {}
  
  get age() {
    return this._age
  }
  
  set age(val) {
    if(val >= 200) {
      this._age = 200
    } else if(val < 0){
      this._age = 0
    } else {
      this._age = Math.floor(val)
    }
  }
}

let user = new User()
user.age = 18.8
console.log(user.age)   // 18

泛型

泛型是指附属于函数、接口、类、类型别名之上的类型。
泛型相当于一个类型变量,在定义时,不预先指定具体的类型,而在使用的时候再指定类型

泛型-函数

定义:在函数名之后写上 <泛型名称>

先看一个例子(从数组中取出前n项)

function take(arr: any[], n: number): any[] {
  if(n >= arr.length){
    return arr
  }
  const newArr: any[] = []
  for(let i = 0; i < n; i++){
    newArr.push(arr[i])
  }
  return newArr
}

const newArr = take(['1','2','3','4','5'], 2)
newArr.forEach(item => {
  // 此时的item是any类型,而我们期望是字符串类型
})

这个例子明显丢失了一些类型信息。如果我们想要的效果是,我们预先不知道会传入什么类型,但是我们希望不管我们传入什么类型,返回数组的值里面的类型应该和参数保持一致,那么这时候泛型就登场了

泛型改造

function take<T = number>(arr: T[], n: number): T[] {  // <T = number>泛型可以设置默认值
  if(n >= arr.length){
    return arr
  }
  const newArr: T[] = []
  for(let i = 0; i < n; i++){
    newArr.push(arr[i])
  }
  return newArr
}

const newArr = take<string>(['1','2','3','4','5'], 2)
newArr.forEach(item => {
  // 此时的item是any类型,而我们期望是字符串类型
})

泛型-类型别名、接口

定义:直接在名称后写上 <泛型名称>

// 类型别名
type callback<T> = (n: T, i: number) => boolean

// 接口
interface callback<T> {
  (n: T, i: number) => boolean
}
function filter<T>(arr: T[], cb: callback<T>): T[] {
  const newArr: T[] = []
  arr.forEach((n, i) => {
    cb(n, i) && newArr.push(n)
  })
  return newArr
}

const newArr = filter([1, 2, 3, 4, 5], n => n % 2 === 0)

泛型-类

定义:直接在名称后写上 <泛型名称>

class MyArr<T> {
  private list: T[] = []
  add(val: T) {
    this.list.push(val)
  }
}

泛型约束

用于限制泛型的取值

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

function nameToUpperCase<T>(obj: T): T {
  console.log(obj.name)  // error,类型“T”上不存在属性“name”
  return obj
}

const user = {
  name: 'ymy',
  age: 22,
  gender: 'female'
}

const newUser = nameToUpperCase(user)

上例中,泛型 T 不一定包含属性 name,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 name 属性的变量。这就是泛型约束

interface hasNameProperty {
  name: string
}

function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
  obj.name = obj.name
    .split(" ")
    .map(s => s[0].toUpperCase() + s.substr(1))
    .join("")
  return obj
}

const user = {
  name: 'ymy',
  age: 22,
  gender: 'female'
}

const newUser = nameToUpperCase(user)
console.log(newUser.name)  // Ymy

注意:我们在泛型里面使用 extends 关键字代表的是泛型约束 需要和类的继承区分开

多泛型

// 将两个数组进行混合。 [1, 2, 3]、['a', 'b', 'c'] -> [1, 'a', '2', 'b', '3', 'c']
function mixinArray<T, U>(arr1: T[], arr2: U[]): (T | U)[] {
  if(arr1.length !== arr2.length) {
    throw new Error("两个数组长度不等")
  }
  let result: (T | U)[] = []
  for(let i = 0; i < arr1.length; i++){
    result.push(arr1[i])
    result.push(arr2[i])
  }
  return result
}

const mixinArr = mixinArray([1, 2, 3], ['a', 'b', 'c'])