TypeScript的学习 | 青训营笔记

130 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

今日学习了TypeScript的基本使用,由于是第一次接触的TypeScript,所以不仅学习了课上的知识,还去其他地方补了基础知识,如TypeScript新增的数据类型、接口以及泛型。

1.TypeScript的运行环境

◼ 在前面我们提到过,TypeScript最终会被编译成JavaScript来运行,所以我们需要搭建对应的环境

  • 我们需要在电脑上安装TypeScript,这样就可以通过TypeScript的Compiler将其编译成JavaScript
  • 所以,我们需要先可以先进行全局的安装
# 安装命令
npm install typescript -g
# 查看版本
tsc --version
# 编译ts文件
tsc math.ts

◼ 如果我们每次为了查看TypeScript代码的运行效果,都通过经过两个步骤的话就太繁琐了:

  • 第一步:通过tsc编译TypeScript到JavaScript代码;
  • 第二步:在浏览器或者Node环境下运行JavaScript代码;

◼ 上面我提到的两种方式,可以通过两个解决方案来完成:

  • 方式二:通过ts-node库,为TypeScript的运行提供执行环境;
  • 方式一:通过webpack,配置本地的TypeScript编译环境和开启一个本地服务,可以直接运行在浏览器上

方案一:通过ts-node库

#方式二:安装ts-node
npm install ts-node -g

#另外ts-node需要依赖 tslib 和 @types/node 两个包
npm install tslib @types/node -g

#现在,我们可以直接通过 ts-node 来运行TypeScript的代码:
ts-node math.ts

方案二:webpack搭建typeScipt运行环境

  1. npm init创建package.json(初始化package.json)
  2. 下载npm install webpack webpack-cli -D(下载webpack)
  3. npm install ts-loader -D(配置ts的loader)
  4. npm install html-webpack-pugin -D(对index.html进行打包处理)
  5. npm install webpack-dev-server -D(创建本地服务)
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  mode: "development",
  entry: "./src/index.ts",
  output: {
    filename: "hundle.js",
    path: path.resolve(__dirname, './build')
  },
  resolve: {
    extensions: ['.ts', '.js', 'cjs', '.json']
  },
  module: {
    rules: [
      {
        test: /.ts$/,
        loader: "ts-loader"
      }
    ]
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'ts',
      template: './index.html'
    })
  ]
}

2.TypeScript的数据类型

any类型

let a: any = "why"
a = 123
a = true
const aArray: any[] = ["why", 18, 1.88]

unknown类型

用法上和any差不多,但是也有所差别

any 类型的变量可以赋值给任意变量。

unknow 类型的变量,只能赋值给any和unknow类型,可以理解为any的类型安全。

void类型

function foo(): void {
    console.log('没有返回值的函数')
}

如果函数没有放回值,可以不写返回类型,默认放回void

tuple类型

const myInfos: [string, number, number] = ['lyh', 18, 1.88]
const item1 = myInfos[0] //'lyh',并且知道类型是string类型
const item2 = myInfos[1] //18, 并且知道类型是number类型

那么tuple和数组有什么区别呢?

首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中。(可以放在对象或者元组 中)
其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型;

枚举类型

enum Direction {
    LEFT,
    RIGHT,
    TOP,
    BOTTOM
}

function turnDirection(direction: Direction) {
    switch (direction) {
        case Direction.LEFT:
            console.log('左')
            break
        case Direction.RIGHT:
            console.log('右')
            break
        case Direction.TOP:
            console.log('上')
            break
        case Direction.BOTTOM:
            console.log('下')
            break
        default:
            const myDirection: never = direction
    }
}

turnDirection(Direction.LEFT)

字面量类型

let message: "Hello World" = "Hello World"

默认情况下这么做是没有太大的意义的,但是我们可以将多个类型联合在一起

type AlignType = 'left' | 'right' | 'center' 
function changeAlign(align: AlignType) {
  console.log("修改发现":align)
}
changeAlign('left')

类的类型

class Person {
  name: string
  constructor(name: string) {
    this.name = name
  }
}
const p1: Person = new Person("why")
const p2: Person = {
  name: "kobe",
  running: function() {
    console.log(this.name + "running")
  }
}

3.TypeScript类型补充

可选类型

对象和函数参数可以指定哪些属性是可选的,可以在属性后面添加一个?

// 可选类型必须也在必选类型后面
function printPoint(x: number, y: number, z?: number) {
    console.log(x, y, z)
}
printPoint(10, 20)
type infoType = {
    name: string,
    family: {
        name: string,
        age?: number
    }
}
const info: infoType  = {
    name: '佩奇',
    family: {
        name: '乔治',
    }
}

联合类型

// 联合类型中的每一个类型被称之为联合成员
function foo(id: string | number) {
    console.log(id)
}

foo(10)
foo('lyh')

交叉类型

type nameObjType = {name: string} 
type ageObjType = {age: number}

const info: nameObjType & ageObjType = {
    name: 'lyh',
    age: 18
}

类型别名

type infoType = {
    name: string,
    family: {
        name: string,
        age?: number
    }
}
const info: infoType  = {
    name: '佩奇',
    family: {
        name: '乔治',
    }
}

类型断言as

有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言

比如我们通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型 (代码如下)

const myEl = document.getElementById("my-img") as HTMLImageElement
myEl.src = "图片类型"

TypeScript只允许类型断言转换为 更具体或者不太具体的类型版本,此规则可防止不可能的强制转换,在非必要的情况下尽量少使用此方法。 (代码如下)

const name = ("coderwhy" as unknown) as number

非空类型断言!

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测

function printMessage(message?: string) {
  console.log(message!.toUpperCase)
}
printMessage('Coder')

空值合并操作符 (??)和逻辑或(||)的区别

空值合并操作符(??): 只有当左侧为null和undefined时,才会返回右侧的数

逻辑或操作符(||): 只有当左侧通过!!会转化为false类型,才会返回右侧的数

类型缩小

① typeof

function foo(arg: number | string) {
    if (typeof arg === 'string') {
        arg.toUpperCase
    }
    console.log(arg)
}

平等缩小

type align = 'left' | 'center' | 'right'
function foo(arg: align) {
    switch (arg) {
        case 'left': 
            console.log('left')
            break
        case 'center':
            console.log('center')
            break
        case 'right':
            console.log('right')
    }
}

③ instanceof

class Programmer {
    coder() {
        console.log('写代码')
    }
}
class Teacher {
    study() {
        console.log('学习')
    }
}

function move(arg: Programmer | Teacher) {
    if (arg instanceof Programmer) {
        arg.coder()
    }else {
        arg.study()
    }
}

④ in

type FishType = {
    name: string,
    swimming: () => void
}
type DogType = {
    name: string,
    runing: () => void
}

function move(animal: FishType | DogType) {
    if ('swimming' in animal) {
        animal.swimming()
    }else {
        animal.runing()
    }
}

const fish:FishType = {
    name: '鱼儿',
    swimming() {
        console.log('游泳')
    }
}

move(fish)

剩余参数

function sum(...arr: number[]) {
    return arr.reduce((prev, item) => prev + item, 0)
}
console.log(sum(1,2,3))

不确定的this类型

function sayHello() {
    console.log(this.name)
}
const info = {
    name: 'why',
    sayHello
}
info.sayHello()

上面这段代码会报错:

  • 这里我们再次强调一下,TypeScript进行类型检测的目的是让我们的代码更加的安全;
  • 所以这里对于 sayHello 的调用来说,我们虽然将其放到了info中,通过info去调用,this依然是指向info对象的;
  • 但是对于TypeScript编译器来说,这个代码是非常不安全的,因为我们也有可能直接调用函数,或者通过别的对象来 调用函数

所以应该指定this的类型

type NameType = {
    name: string
}

function sayHello(this: NameType) {
    console.log(this.name)
}
const info = {
    name: 'why',
    sayHello
}
info.sayHello()

3.TypeScript函数

函数表达式的类型

type sumType = (arg1: number, arg2: number) => number
const sum: sumType = function(num1: number, num2: number): number {
    return num1 + num2
}

函数类型的参数个数问题

问题引出:明明bar中只有一个参数,calcFn中却要求传入两个参数,为什么bar传入不会报错??

interface ICalcFn {
    (n1: number, n2: number) : void
}

function calc(calcFn: ICalcFn) {
    const num1 = 10
    const num2 = 20
    const res = calcFn(num1, num2)
    console.log(res)
}

const foo = function(num1: number, num2: number): number {
    return num1 + num2
}

const bar = function(num1: number): number {
    return num1 
}
calc(foo)

calc(bar) //明明bar中只有一个参数,calcFn中却要求传入两个参数,为什么bar传入不会报错??

这是因为如果限制了函数参数的数量,数组掉调用forEach的时候就要完整的传入item、index、arr三个参数。这样会导致ts变的不太好用。


函数的调用签名

// 函数的调用签名
interface IBar {
    name: string
    age: number
    (num: number): number
}
const bar: IBar = (num: number): number => {
    return 123
}

bar.name = 'aaa'
bar.age = 18
bar(123)

函数的构造签名

class Person {

}
interface ICTORPerson {
    new(): Person
}

function factory(fn: ICTORPerson) {
    const f = new fn()
    return f
}

factory(Person)

函数的重载

function sum(a1: number, a2: number): number
function sum(a1: string | number, a2: string): string
function sum(a1: string, a2: string | number): string
function sum(a1: any, a2: any): any {
    return a1 + a2
}

函数体执行前,会先看一下声明类型部分有没有匹配,匹配才会执行函数体。

在可能的情况下,尽量选择使用联合类型来实现

4.TypeScript面向对象

类的继承

我们使用extends关键字来实现继承,子类中使用super来访问父类。

  • 我们来看一下Student类继承自Person:
    • Student类可以有自己的属性和方法,并且会继承Person的属性和方法;
    • 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;
class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    name: string
    age: number
    running() {
        console.log('跑步')
    }
}

class student extends Person {
    constructor(name, age, id) {
      // 调用父类的构造器
        super(name, age)
        this.id = id
    }
    id: number
}

类的多态

多态的目的是为了写出更具通用性的代码

class Animal {
    active() {
        console.log('行走')
    }
}
class Fish extends Animal {
    active(): void {
        console.log('游动')
    }
}
class Dog extends Animal {
    active(): void {
        console.log('跑动')
    }
}

// 父类引用(类型)指向子类对象
function getActives(animals: Animal[]) {
    animals.forEach(animal => {
        animal.active()
    })
}

getActives([new Fish(), new Dog()])

类的成员修饰符

  • 在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
    • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
    • private 修饰的是仅在同一类中可见、私有的属性或方法;可以通过方法get和set私有的属性
class Persion {
    private name: string = '刘德华'
    getName() {
        return this.name
    }

    setName(name: string) {
        this.name = name
    }
}
const p = new Persion()
const res = p.getName()
console.log(res)
    • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
class Persion {
    protected name: string = '刘德华'
}

class Boy extends Persion {
    constructor() {
        super()
    } 
    getName() {
        return this.name
    }
}
const cxk = new Boy()
const res = cxk.getName()
console.log(res)

只读属性 readonly

如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly,在constructor中可以修改

getters/setters

在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程, 这个时候我们可以使用存取器。

class Person {
    private _name: string = '蔡徐坤'
    get name() {
        return this._name
    }

    set name(newValue) {
        this._name = newValue
    }
}

const p = new Person()
p.name = '刘德华'
console.log(p.name)

静态成员

class Person {
    static fullName: string = '蔡徐坤'
    static age: number = 18
    
}
console.log(Person.fullName)
console.log(Person.age)

抽象类abstract

  • 我们知道,继承是多态使用的前提。
    • 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
    • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
  • 什么是抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。
    • 抽象方法,必须存在于抽象类中;
    • 抽象类是使用abstract声明的类;
  • 抽象类有如下的特点:
    • 抽象类是不能被实例的(也就是不能通过new创建);
    • 抽象方法必须被子类实现,否则该类必须是一个抽象类;
// 抽象类
abstract class shape {
    abstract getArea()
}

// 矩形类
class Rectangle extends shape {
    constructor(weight: number, height: number) {
        super()
        this.weight = weight
        this.height = height
    }
    weight: number
    height: number
    getArea( ) {
        return this.weight * this.height
    }
}

// 圆形类
class Circular extends shape {
    constructor(radius: number) {
        super()
        this.radius = radius
    }
    radius: number
    getArea() {
        return  3.14 * (this.radius ** 2)
    }
}
// 矩形实例
const r = new Rectangle(10, 20)
// 圆形实例
const c = new Circular(10)

function makeArea(shape: any) {
    return shape.getArea()
}
console.log(makeArea(r), makeArea(c))

5.TypeScript接口

接口的声明

interface IInfoType  {
    name: string
    age: number
}

const info: IInfoType = {
    name: 'why',
    age: 18
}

只读属性

interface IInfoType  {
    readonly name: string
    age: number
}

const info: IInfoType = {
    name: 'why',
    age: 18
}

索引签名

interface IBookType {
    [index: number]: string
}

const info: IBookType = {
    0: 'HTML',
    1: 'JavaScript',
    2: 'CSS',
}

接口继承

  • 接口和类一样是可以进行继承的,也是使用extends关键字:
    • 并且我们会发现,接口是支持多继承的(类不支持多继承)
interface Person {
    name: string
    eating: () => void
}
interface Animal {
    runing: () => void
}

interface Student extends Person, Animal {
    id: number
}

const student:Student = {
    id: 111,
    name: 'why',
    eating() {
        console.log('吃饭')
    },
    runing() {
        console.log('跑步')
    }
}

接口类的实现


interface Iswim {
    swimming: () => void
}
interface Irun {
    running: () => void
}
class Person implements Iswim, Irun {
    swimming() {
        console.log('游泳')
    }
    running() {
        console.log('跑步')
    }
}

interface和type区别

  • 我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
    • 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
  • 如果是定义对象类型,那么他们是有区别的:
    • interface 可以重复的对某个接口来定义属性和方法;

    • 而type定义的是别名,别名是不能重复的;

字面量赋值( freshness擦除

第一次创建的对象字面量,称之为fresh(新鲜)
对于新鲜的字面量,会进行严格的类型检查,必须完全满足类型的要求(不能有多余的属性)

interface IInfo  {
    name: string
    age: number
    sing: () => void
}

const obj = {
    name: '蔡徐坤',
    age: 18,
    sing() {
        console.log('游泳')
    },
    hobby() {
        console.log('跳舞')
    }
}
// 将一个 变量标识符 赋值给其他的变量时,会进行freshness擦除操作。
const info: IInfo = obj

6.TypeScript泛型

泛型实现类型参数化

function sum<Type>(num: Type) {
    return num
}
sum<number>(2)

泛型传入多个类型

function sum<T, E>(num: T, str: E) {
    return num
}
sum<number, string>(2, '123')

泛型接口

interface IInfo<T> {
    name: string
    age: number
    hobby: T
}

const info: IInfo<() => void> = {
    name: '蔡徐坤',
    age: 18,
    hobby() {
        console.log('rap')
    }
}

泛型类

class Person<T, E> {
    name: T
    age: E
    constructor(name: T, age: E) {
        this.name = name
        this.age = age
    }
}

const cxk = new Person<string, number>('蔡徐坤', 18)
console.log(cxk)

泛型约束

interface IgetLength {
    length: number
}

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

const res = getLength<string>('你好,李银河')

console.log(res)//6