你好,TypeScript!

247 阅读16分钟

这篇文章主要收录了ts与js的关系、类型检测的共识、搭建编译ts环境、ts中变量声明、数据类型、类型别名、类型断言、可选链、类型缩小、函数重载、类、接口、枚举、泛型、模块化、类型查找等相关知识

1 js与ts的关系

1.js本身没有对变量、函数参数进行类型限制;

2.这样虽然灵活但是也有安全隐患;

3.之后facebook推出了flow微软推出了ts,他们都是致力于为js提供类型检查,而不是取代;

4.并且在ts的官方文档上有这么一句话:源于js,归于js! ;

5.最终ts还是需要转换成js代码才能运行的;

6.当然不排除有一天js本身也会加入类型检测,那么无论是ts还是flow都有可能因此退出历史舞台;

2 类型检测

2.1 共识

错误出现的越早越好

能在写代码时发现错误,就不要再代码编译时再发现(IDE的优势就是在代码编写过程中帮助我们发现错误);

能在代码编译时发现错误,就不要再代码运行时再发现(类型检测就可以很好的帮助我们做到这一点);

能在开发阶段发现错误,就不要在测试期间发现错误;

能在测试期间发现错误,就不要在上线后发现错误;

来看这么一段代码

function foo(message) {
  console.log(message.length)
}
foo('hello')
// 后面有其它代码

运行,结果是5

这么看来这段代码基本是没有什么问题的;

但是存在一个非常大的隐患:当执行foo() 不传任何参数时,就报错了;

由于没传参数,导致foo()中的message是undefined,undefined哪来的length?

这个报错导致后面的代码无法执行,这就导致一行报错,后面所有代码都无法运行,这是你希望看到的吗?;

上面函数并没有对参数进行校验

  • 参数类型
  • 是否传参

这时,TypeScript应运而生。

3 TypeScript

ts是拥有类型js超集,它可以编译成普通、干净、完整的js代码。

js所有的特性,ts都支持,并且它紧随ECMAScript的标准,所以es6、es7、es8等新语法标准,ts都是支持的;

在语言层面上,不仅仅增加了类型约束,而且包括一些语法的拓展,比如枚举类型(Enum)、元组类型(Tuple)等;

所以,可以将ts理解为加强版的js

从开发者长远的角度看来,学习ts有助于培养类型思维,这种思维对于完成大型项目尤为重要。

TypeScript官网:www.typescriptlang.org/

3.1 编译环境

我们知道ts是需要转换成js的,那谁来负责转换呢?

  • tsc,TypeScript Compiler
  • babel

全局安装

npm install typescript -g

tsc --version 查看版本

3.2 体验

来编写一段ts代码

function foo(message: string) {
  console.log(message.length)
}
foo('hello')

当你执行foo()不传参数或者传入非string类型的参数,编辑器就会提醒你了,不需要等到运行期间才报错;

然后执行 tsc ts文件 命令,转化为js文件,这时会出现同名的js文件;

转化后的代码

function foo(message) {
  console.log(message.length)
}
foo('hello')

3.3 搭建ts编译环境

当然,真实开发不是写一个ts文件转换一个

我们需要:

  • 编写完ts,它自动转换成js;
  • 然后自动在浏览器上运行
  • 也可以自动更新内容;

搭建方式有几种:

  • 通过webpack搭建;
  • 安装node的一个库ts-node;

使用ts-node库搭建

ts-node做了什么事情?

将ts转换成js,然后在node环境上运行

先全局安装 npm install ts-node -g;

而ts-node又依赖于两个库,tslib、@types/node,全局安装它们 npm install tslib @types/node -g

测试

编写这么一段ts代码

const name: string = 'abc'
const age: number = 18console.log(name)
console.log(age)
​
export {}

为什么要写 export {}

因为ts文件默认使用全局作用域(所有的ts文件),使用export语法变成模块,就有独立的作用域了,防止命名冲突

执行 ts-node ts文件 命令,你会看到编辑器的终端打印abc和18

使用webpack搭建

本地安装ts-loadertypescript,因为ts-loader本身又依赖于typescript

npm install ts-loader typescript -d

配置webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /.ts$/,
                loader: ts-loader
            }
        ]
    }
}

同时还需要tscconfig.js这个属于ts的配置文件,可以使用 tsc --init 生成;

最后还需要搭建一个本地服务,利用webpack-dev-server;

本地安装

npm install webpack-dev-server -d

3.4 变量声明

声明的类型可以称之为类型注解

let/const 标识符: 类型 = 值

例子

let msg: string = 'hhh'

小写和大写区别

类型的小写:TypeScript中的类型;

类型的大写:JavaScript中的类型对应的包装类

类型推断

默认情况下进行赋值时,会将赋值的类型,作为标识符的类型

例子

let msg = 'hhh'

msg默认的类型就是字符串类型

3.5 数据类型

  • number类型
  • boolean类型
  • 字符串类型
  • Array类型
  • object类型
  • null和undefined
  • Symbol类型
  • any类型
  • unknown类型
  • void类型
  • never类型
  • tuple类型

Array类型

规定一个数组类型并且元素是string类型的例子

写法一,不推荐,可能会和jsx冲突

const names: Array<string> = []

写法二,推荐

const names: string[] = []

any类型

类型不限制

tuple类型

区别于数组类型,数组的元素类型一般一致,只有 “共性”

而元组可以保留元素的 “个性”

元组通常可以作为返回的值,在使用时会非常方便;

const zsf: [string, number, number] = ['hhh', 18, 100]

3.6 联合类型

扩大类型的范围

ts的类型系统允许使用多种运算符,从现有的类型中构建新类型;

联合类型中每个类型被称之为联合成员(union’s members);

function printID(id: number | string) {
  console.log(id)
}

联合类型可选类型有点类似

function printID(id?: number) {
  console.log(id)
}

可以转换成

function printID(id: number | undefined) {
  console.log(id)
}

结合字面量类型

其实字符串也是当成类型,也就是字面量类型;

而字面量类型的只能和类型一致;

const msg: 'hhh' = 'hhh'

这时字面量类型就可以和联合类型结合使用了,不和结合类型结合使用,字面量类型没什么意义

let align: 'left' | 'right' | 'center'
align = 'right'

3.7 类型别名

当联合类型有很多联合成员时,类型会非常长,可读性可能不好;

此时可以给类型起别名,使用关键字type定义;

type IdType = number | string
type PointType = {
    x: number, 
    y: number, 
    z?: number
}

function printID(id: IdType) {
  console.log(id)
}

function printPoint(point: PointType) {
  console.log(point.x, point.y, point.z)
}

3.8 类型断言as

有时候ts无法获取具体的类型信息,这时就需要使用类型断言(Type Assertions);

比如document.getElementById(),ts只知道该函数会返回HTMLElement,但并不知道它具体的类型

通过类型断言可以把一个范围较大的类型转化为更为具体的类型;

比如这一段代码

const el = document.getElementById('hhh')
el.src = '...'

当直接使用src属性时,编辑器会提示错误,因为HTMLElement类型有很多,有的并没有src属性;

而将类型断言为HTMLImageElement,一定有src属性;

const el = document.getElementById('hhh') as HTMLImageElement
el.src = '...'

非空类型断言

看这一段代码

function foo(msg?: string) {
  console.log(msg.length)
}

这段编译阶段是不通过的,参数是可选类型,当没传参数时msg就是undefined,undefined哪来的length?

一般可以加个if判断;

function foo(msg?: string) {
  if(msg) {
      console.log(msg.length)
  }
}

也可以使用非空类型判断

function foo(msg?: string) {
  console.log(msg!.length)
}

感叹号!就可以确保msg一定有值;

3.9 可选链

可选链并不是ts独有的特性,在es11中js也增加了这一特性;

操作符是?;

它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,才会继续执行;

type Person = {
  name: string,
  friend?: {
    name: string,
    age?: number
  }
}
const info: Person = {
  name: 'zsf'
}

console.log(info.friend?.name)

3.10 ??和!!

!!操作符将其它类型转化为boolean类型

const msg: string = 'hh'
const flag = !!msg
console.log(flag)

空值合并运算符?? ,es11新增,类似于三元运算符

let msg: stirng | null = null
const content = msg ?? 'hhh'
console.log(content)

等价于

let msg: stirng | null = null
const content = msg ? msg: 'hhh'
console.log(content)

3.11 类型缩小

可以通过类似typeof padding === 'number'的判断语句来改变ts的执行路径

在给定的执行路径中,可以缩小比声明时更小的类型,这个过程叫类型缩小,也叫类型保护

常见的类型保护有:

  • typeof
  • 平等缩小(===、!==)
  • instanceof
  • in
  • 等等
type IDType = number | string
function printID(id: IDType) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase())
  } else {
    console.log(id)
  }
}

本来是number和string的联合类型typeof id === 'string'这个判断将类型缩小为string类型

这样才能使用toUpperCase() ,number类型是没有这个方法的;

3.12 函数相关的类型

参数

function sum(num1: number, num2: number) {
  return num1 + num2
}

匿名函数的参数类型

const names = ['hhh', 'abc', 'nb']
names.forEach((item) => {
    console.log(item.split(''))
})

item的类型可以根据上下文推断出来,这时可以不加类型注解;

复杂的参数一般用对象类型

比如打印一个点坐标的函数,参数是坐标,较为复杂,可以使用对象类型限制

function printPoint(point: {x: number, y: number}) {
  console.log(point.x, point.y)
}

返回值

可以不写返回值类型,会自动推断;

function sum(num1: number, num2: number): number {
  return num1 + num2
}

如果没有返回值

function sum(num1: number, num2: number): void {
  return num1 + num2
}

可选类型

有些点没有z坐标,所以z参数可选;

可选类型放必选类型后面

function printPoint(point: {x: number, y: number, z?: number}) {
  console.log(point.x, point.y, point.z)
}

函数的类型

在js中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值);

既然ts中传递参数要类型注解,那把函数当参数传递时应该怎么写类型注解呢?

function foo() {}
type FooType = () => void
function bar(fn: FooType) {
  fn()
}
bar(foo)

声明函数时,另一种编写类型的方式

type AddFnType = (num1: number, num2: number) => number
const add: AddFnType = (a1: number, a2: number) => {
  return a1 + a2
}

可推导的this类型

this在不同情况下会绑定不同的值,所以对于它的类型就更难把握了;

默认推导

const info = {
  name: 'zsf',
  eating() {
    console.log(this.name + 'eating')
  }
}
info.eating()

ts默认推导this就是info对象;

如果ts能推导出this,那就可以放心使用this;

但是下面这种情况ts推导不出this

function eating() {
  console.log(this.name + 'eating')
}
const info = {
  name: 'zsf',
  eating: eating
}
info.eating()

这段代码如果放在js中,this绑定的是info对象;

但在ts中,它推导不出this,要是想能让ts推导出,第一个参数得传this;

type ThisType = {
    name: string
}
function eating(this: ThisType) {
  console.log(this.name + 'eating')
}
const info = {
  name: 'zsf',
  eating: eating
}
info.eating()

3.13 函数的重载

先来实现一个简单的函数:对两个数字或者字符串执行+的操作

以前的做法: 使用联合类型,以及一些逻辑判断(类型缩小)

type AddType = number | string
function add(a1: AddType, a2: AddType) {
  if (typeof a1 === 'number' && typeof a2 === 'number') {
    return a1 + a2
  } else if(typeof a1 === 'string' && typeof a2 === 'string') {
    return a1 + a2
  }
}
add(10, 20)

使用联合类型实现有两个缺点:

  • 进行很多的逻辑判断(类型缩小);
  • 返回值的类型依然是不能确定的;

这时使用函数重载就可以啦

ts中的函数重载是函数名一样,参数不一样(类型、数量),不用具体实现

而函数的具体实现,参数用的是广泛的any类型

function add(num1: number, num2: number): number 
function add(num1: string, num2: string): string
function add(num1: any, num2: any) {
  return num1 + num2
}
add(10, 20)

有时候,联合类型函数重载都可以实现某个逻辑,优先使用联合类型,如果联合类型实现起来复杂,那才考虑重载

3.14 ts中类的使用

基本使用

class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  eating() {
    console.log(this.name + 'eat')
  }
}

const p = new Person('zsf', 18)

继承,多态,重写等等语法类似~

成员修饰符

在TypeScript中,类的成员支持三种修饰符:public、private、protected;

public修饰任何地方可见公有的成员,默认;

protected修饰的是仅在类自身及子类中可见受保护的成员;

private修饰的是仅在同一类中可见私有的成员;

只读属性

readonly,是属性的修饰符

只读属性只能在构造器中赋值,且赋值后不可改变;

属性本身不能修改,但属性要是对象类型,那对象的内容可以修改(和const声明的对象类似);

class Person {
  readonly name: string

  constructor(name: string) {
    this.name = name
  }
}

const p = new Person('zsf')
console.log(p.name)

访问器

和setter/getter写法有点区别;

有个规范,私有属性一般在前面加个下划线_;

class Person {
  private _name: string

  constructor(name: string) {
    this._name = name
  }
  
  set name(newName) {
      this._name = newName
  }
  
  get name() {
      return this._name
  }
}

const p = new Person('zsf')
console.log(p.name)
p.name = 'hhh'

静态成员

static修饰

不用实例化,可以通过类直接访问

抽象类

定义通用接口时,通常会让调用者传入父类,通过多态来实现更加灵活的调用方式;

但是,父类本身可能并不需要对某些方法进行具体实现,这时就可以定义为抽象方法

function makeArea(shape: Shape) {
  return shape.getArea()
}

abstract class Shape {
  abstract getArea()
}

class Rectangle extends Shape {
  private width: number
  private height: number

  constructor(width: number, height: number) {
    super()
    this.width = width
    this.height = height
  }

  getArea() {
    return this.width * this.height
  }
}

class Circle extends Shape {
  private r: number

  constructor(r: number) {
    super()
    this.r = r
  }

  getArea() {
    return this.r * this.r * 3.14
  }
}

const rectangle = new Rectangle(20, 30)
const circle = new Circle(10) 

console.log(makeArea(rectangle))
console.log(makeArea(circle))

防止makeArea()传入其它类型导致无法计算面积,应该限制只能传Shape类型;

所以CircleRectangle应该继承Shape

抽象类不能被实例化,防止makeArea(new Shape()) ,应该将Shape类变成抽象类;

抽象类的抽象方法没有具体实现(没有方法体),具体实现交给子类

抽象类的子类,必须实现其父类的抽象方法;

所以你看,这样的代码不是严谨、安全许多了吗?

类的类型

和使用type给类型起别名有点类似~

class Person {
  name: string
}

const p: Person = {
  name: 'hhh'
}

3.15 ts中接口的使用

我们知道,通过type可以声明对象类型

type InfoType = {
  name: string
}

const info: InfoType = {
  name: 'hhh'
}

其实,还可以通过interface声明对象类型,用法类似class;

有个接口规范,就是在接口前面加个大写字母I

class IInfoType {
  name: string
}

const info: IInfoType = {
  name: 'hhh'
}

索引类型

通过interface可以定义索引类型,使对象的key-value保持类型的统一;

interface IndexLang {
  [index: number]: string
}

const frontLang: IndexLang = {
  0: 'html',
  1: 'css',
  2: 'js',
  3: 'vue'
}

函数类型

以前可以通过type定义一个函数类型

type CalcFn = (n1: number, n2: number) => number
function calc(num1: number, num2: number, calcFn: CalcFn) {
  return calcFn(num1, num2)
}

const add: CalcFn = (num1, num2) => {
  return num1 + num2
}

calc(10, 20, add)

interface也可以定义一个函数类型

interface CalcFn {
    (n1: number, n2: number): number
}
function calc(num1: number, num2: number, calcFn: CalcFn) {
  return calcFn(num1, num2)
}

const add: CalcFn = (num1, num2) => {
  return num1 + num2
}

calc(10, 20, add)

接口继承

接口支持多继承,这是结合多个接口的一个方式;

interface ISwim {
  swimming: () => void
}
interface IFly {
  flying: () => void
}

interface IAction extends ISwim, IFly {

}

const action: IAction = {
  swimming() {

  },
  flying() {
    
  }
}

交叉类型

交叉类型和联合类型有所联系;

联合类型只要符合一个即可;

交叉类型需要全符合

这是结合多个接口的一个方式;

interface ISwim {
  swimming: () => void
}
interface IFly {
  flying: () => void
}
type MyType1 = ISwim | IFly
type MyType2 = ISwim & IFly

const action1: MyType1 = {
  swimming() {

  }
}
const action2: MyType2 = {
  swimming() {

  },
  flying() {

  }
}

接口的实现

可以实现多个接口;

interface ISwim {
  swimming: () => void
}

interface IFly {
  flying: () => void
}

class Person implements ISwim {
  swimming() {
    console.log('swim')
  }
    
  eating() {
      console.log('eat')
  }
}

interface和type的区别

interface和type都可以定义对象类型,那开发中选哪一个呢?

如果定义的是非对象类型,推荐使用type

如果定义对象类型,可以interface重复的定义某个接口来定义属性和方法,而type定义的是别名,不能重复

interface IFoo {
  name: string
}

interface IFoo {
  age: number
}

const p: IFoo = {
    name: 'zsf',
    age: 18
}

3.16 ts中的枚举类型

枚举类型其实是将一组可能出现的值,列举出来,定义在一个类型中;

枚举运行定义一组命名常量,常量可以是数字字符串

enum Direction {
  LEFT,
  RIGHT,
  TOP,
  BOTTOM
}

function turn(direction: Direction) {
  
}

turn(Direction.LEFT)

3.17 ts中泛型的使用

类型参数化

定义函数时,不决定参数类型;

调用函数时,让调用者告知参数类型;

换句话说,参数的类型也是参数

function sum<Type>(num1: Type, num2: Type) {
  
}
sum<number>(20, 30)

调用时要是不传参数类型,它也会进行类型推导,字面量类型;

function sum<Type>(num1: Type, num2: Type) {
  
}
sum(20, 30)

当然也可以接收多种类型;

function sum<T, E>(num1: T, num2: E) {
  
}
sum<number, string>(20, 30)

平时开发中常用的名称:

  • T:Type的缩写,类型;
  • K、V:key和value的缩写,键值对;
  • E:Element的缩写,元素;
  • O:Object的缩写,对象;

泛型接口

函数调用才有类型推导,但是接口没有类型推导;

interface IPerson<T1, T2> {
  name: T1,
  age: T2
}

const p: IPerson<string, number> = {
  name: 'zsf',
  age: 18
}

要想接口也可以类型推导,需要给泛型默认值

interface IPerson<T1 = string, T2 = number> {
  name: T1,
  age: T2
}

const p: IPerson = {
  name: 'zsf',
  age: 18
}

泛型类

class Point<T> {
  x: T
  y: T
  z: T

  constructor(x: T, y: T, z: T) {
    this.x = x
    this.y = y
    this.z = z
  }
}

const p = new Point('1', '2', '3')

new Point()也是函数调用,支持类型推导~

泛型的类型约束

interface ILength {
  length: number
}

function getLength<T extends ILength>(arg: T) {
  return arg.length
}

getLength('abc')

只是含有length属性的参数才能传进getLength() ,

3.18 ts的模块化开发

ts的模块化开发有两种:

  • ES Module和CommonJS;
  • 命名空间;

ES Module不再多说,就是js加上类型检测;

命名空间

namespace

早期时,TypeScript称命名空间为内部模块,解决命名冲突问题;

namespace time {
  export function format(time: string) {
    return '2022-02-22'
  }
}

namespace price {
  export function format(price: number) {
    return '99.99'
  }
}

一个模块中有两个同名函数,使用命名空间可以让他们有独立的作用域,防止命名冲突;

3.19 类型查找

ts对类型是有管理查找规则的;

ts文件除了以.ts结尾,还有以.d.ts结尾的,它是用来做类型声明(declare)的;

它仅仅用来做类型检测,告知ts有哪些类型,不需要具体实现或赋值

那ts会在哪里查找类型声明呢?

  • 内置类型声明;
  • 外部定义的类型声明(第三方发库有的会声明);
  • 自己定义的类型声明;

内置类型声明

看这么一段代码

const el = document.getElementById('hhh') as HTMLImageElement
el.src = '...'

在tsc环境下,哪来的document、HTMLImageElement?为什么不会报错?

原因是我们之前搭建ts环境时,也安装了一个tslib库,而tslib库有个lib.dom.d.ts文件,其中内置document、HTMLImageElement等等类型;

内置类型除了DOM API,还包括Math、Date等内置类型;

外部定义类型声明

第三方库一般有两种类型声明方式:

  • 1.在自己库中进行类型声明(编写.d.ts文件,比如axios);
  • 2.通过社区的一个公有库DefinitelyTyped存放类型声明文件;

DefinitelyTyped库的github地址:github.com/DefinitelyT… ;

该库查找声明安装方式的地址:www.typescriptlang.org/dt/search?s… ;

比如安装react 的类型声明:npm i @types/react --save-dev

自己定义类型声明

项目下新建一个xxx.d.ts文件

declare module 'lodash' {
  export function join(arr: any[]): void
}

别的文件使用

import lodash from 'lodash'
console.log(lodash.join('abc', 'cba'))

文件模块是这样声明的

declare module '*.jpg'

这样,所有以.jpg结尾的模块都可以在ts中使用啦~

其它文件也是类似这个用法

同时declare也可以声明命名空间变量等等;