一篇搞懂typescript

183 阅读16分钟

前言

概述

TypeScript 是一种开源的编程语言,它是 JavaScript 的超集,添加了静态类型检查和一些其他功能。它由微软开发并维护,旨在提供更好的开发工具和更高的可维护性。

特点和优势

  1. 静态类型检查: TypeScript 强调在开发过程中进行静态类型检查。这意味着可以在编译时捕获和纠正常见的类型错误,减少在运行时出现的错误。类型检查可以提供更好的代码可靠性和可维护性。
  2. 类型注解和推断: TypeScript 允许开发人员为变量、函数参数、返回值等添加类型注解。这些注解提供了更丰富的类型信息,使代码更具可读性,并帮助 IDE 提供更好的代码补全和错误检查。此外,TypeScript 也能够根据上下文推断出类型,减少了手动注解的工作量。
  3. ESNext 支持: TypeScript 支持最新的 ECMAScript(JavaScript)语法和功能。这意味着可以使用箭头函数、解构赋值、类、模块等现代 JavaScript 特性,以更直观和简洁的方式编写代码。
  4. 类型系统和面向对象编程: TypeScript 提供了丰富的类型系统,包括基本类型、联合类型、交叉类型、泛型等。这使得可以更精确地描述数据结构和函数签名,提高代码质量和可维护性。此外,TypeScript 还支持面向对象编程的概念,如类、接口、继承、多态等。
  5. 工具生态系统: TypeScript 配备了丰富的开发工具和生态系统。它与各种编辑器和 IDE(如 Visual Studio Code)集成良好,并提供了强大的代码补全、重构、调试和错误检查等功能。此外,TypeScript 还支持常见的构建工具和测试框架,如 Webpack、Babel、Jest 等。
  6. 渐进增强: TypeScript 可以与现有的 JavaScript 代码进行无缝集成。你可以将 TypeScript 文件(.ts 或 .tsx)与 JavaScript 文件(.js 或 .jsx)一起使用,并逐步将 JavaScript 代码迁移到 TypeScript。

基本内容详解

基础类型

TypeScript 支持与 JavaScript 几乎相同的数据类型,并在此基础上扩展了自己的数据类型

JavaScript 数据类型

字符串类型:String 数字类型:Number 布尔类型:Boolean 对象类型:Object 数组类型:Array 函数类型:Function Symbol、undefined、null 其中数组,函数实际也是对象类型,是对象的特殊形式

TypeScript 新增数据类型

void: 用于标识方法返回值的类型,表示该方法没有返回值。
any: 声明为 any 的变量可以赋予任意类型的值。
unknown: 类型安全的any
never: never 类型表示的是那些永不存在的值的类型。
tuple: 元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型必须相同。
枚举: 枚举成员值只读,不可修改。 枚举类型是对 JavaScript 标准数据类型的一个补充

类型声明

基本类型声明

在ts中,当我们定义变量时,需要给他定义一个类型。ts同时支持类型推断,如果定义变量时给变量赋了初始值,ts会默认把变量的类型定义为初始值的类型

// 定义变量并指定类型
let name: string
name = '旺财'
name = 123 // 报错

// 自动推断类型
let name = '旺财'
name = '小黑'
name = 123 // 报错

// 其他基本类型声明
let age: number // 声明数字类型
let isMan: boolean // 声明boolean类型

其他类型声明

数组类型: 数组类型有两种声明方式

// 声明字符串类型的数组
let userList: string[]
// 或者
let userList: Array<string>

元组类型: 元组类型是一种特殊的数组,声明时指定数组长度和数组元素类型

// 声明元组类型
let userList: [string, number]
// 赋值
userList = ['abc', 123]

对象类型

// 定义对象,表示userInfo的类型是一个对象
let userInfo: {
  name: string,
  age: number
}
// 对象类型定义,赋值时,属性名要完全一致,并且不能添加和缺少属性
userInfo = {
  name: '旺财',
  age: 3
}
// 定义可选属性
let userInfo: {
  name: string,
  age?: number
}
// 这样定义表示,赋值时必须包含name属性,age可有可无,但是依旧不能添加其他属性

函数类型

/**
该声明表示setUserInfo为一个函数,并且需要两个参数,并指定参数名,并且
该函数无返回值
*/
let setUserInfo: (name: string, age: number) => void

setUserInfo = (name: string, age: number) => {
  console.log(name, age)
}

// 调用函数时,需要根据参数类型传参
setUserInfo('旺财', 3)

// 参数可设置可选参数,这样表示传参是第二个参数可传可不传,不过如果传就必须是number类型
setUserInfo = (name: string, age?: number) => {
  console.log(name, age)
}

// 有返回值的函数声明
let setUserInfo: (name: string, age: number) => string

setUserInfo = (name: string, age: number): string => {
  return name + number
}

交叉类型: 使用& 符号连接,多个类型合并为一个类型,新的类型具有所有类型的特性。

let userInfo: { name: string } & { age: number }
// 等价于
let userInfo: {
  name: string,
  age: number
}

联合类型: 取值可以为多种类型中的一种

let a: string | number
a = 'abc' // 正确
a = 123 // 正确

将类型抽离成单独变量

使用type关键字,可以单独定义类型,多个变量可共同使用该类型

// 表示user01,user02的类型均为{ name: string, age: number }

type userInfoType = {
  name: string,
  age: number
}

let user01: userInfoType
let user02: userInfoType

在 TypeScript 中,类(Class)是一种用于创建对象的模板或蓝图。类提供了一种组织数据和相关行为的机制,使得代码更加模块化、可维护和可扩展,定义类使用class关键字

创建类

class Person {
  // 内容
}

类的属性

实例属性: 实例属性需要创建类的实例化对象后,通过实例化对象才能读取和修改

class Person {
    // 定义实例属性
    name: string = '张三'
}

// 实例化对象
const p = new Person()
// 调用属性
console.log(p.name) // 张三

// 修改属性
p.name = '李四'
// 调用属性
console.log(p.name) // 李四

静态属性: 静态属性使用static修饰符修饰,静态属性不需要创建实例,直接通过类名便可读取和修改

class Person {
    // 定义静态属性
    static sex: string = '男'
}

// 获取静态属性
console.log(Person.sex) // 男

// 修改静态属性
Person.sex = '女'
console.log(Person.sex) // 女

只读属性: 只读属性使用readonly修饰,可修饰实例属性和静态属性,被修饰的属性只能读取,不能修改

class Person {
    // 定义只读实例属性
    readonly name: string = '张三'
    // 定义只读静态属性
    static readonly age: number = 10
}

// 获取静态属性
console.log(Person.age) // 10
// 获取实例属性
let p = new Person()
console.log(p.name) // 张三
// 修改静态属性
Person.age = 20 // 报错
// 修改实例属性
p.name = '李四' // 报错

公共属性: 公共属性使用pubic修饰,可修饰实例属性和静态属性,被修饰的属性可以在外部被访问,可以进行读取和修改。在不加任何修饰符的情况下,实例属性和静态属性都默认为公共属性。

class Person {
    // 定义公共实例属性
    public name: string = '张三'
    // 定义公共静态属性
    public static age: number = 12
}

// 获取静态属性
console.log(Person.age) // 12
// 获取实例属性
let p = new Person()
console.log(p.name) // 张三
// 修改静态属性
Person.age = 13
console.log(Person.age) // 13
// 修改实例属性
p.name = '李四'
console.log(p.name) // 李四

私有属性: 公共属性使用private修饰,可修饰实例属性和静态属性,被修饰的属性不可以在外部被访问,只可以在类的内部被访问。

class Person {
    // 定义私有实例属性
    private name: string = '张三'
    // 定义私有静态属性
    private static age: number = 18
}

// 获取类的实例属性
const p = new Person()
console.log(p.name) // 报错,私有属性不能在类的外部访问
// 获取类的静态属性
console.log(Person.age) // 报错,私有属性不能在类的外部访问

保护属性: 保护属性使用protected修饰,可修饰实例属性和静态属性,被修饰的属性不可以在外部被访问,只可以在类的内部或者其子类中被访问

class Person {
    // 定义受保护实例属性
    protected name: string = 'jack'
    // 定义受保护静态属性
    protected static age: number = 18
}

class Student extends Person {
    // 在子类中可以访问父类的受保护属性
    studentName: string = this.name
    // 在子类中可以访问父类的受保护静态属性
    studentAge: number = Student.age
}

// 直接获取父类的受保护属性和受保护静态属性
console.log(Person.age) // 报错
const p = new Person()
console.log(p.name) // 报错

// 通过子类获取父类的受保护属性和受保护静态属性
const stu = new Student()
console.log(stu.studentName) // jack
console.log(stu.studentAge) // 18

类的构造函数和this指向

类的构造函数是在创建类的实例时自动调用的特殊方法。构造函数用于初始化类的实例,并设置其初始状态。在 TypeScript 中,构造函数通过 constructor 关键字定义在类中。
初始化属性

class Person {
    // 定义实例属性类型
    name: string
    age: number
    // 定义构造函数
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

// 定义构造函数,语法糖写法
class Person1 {
    // 定义构造函数
    constructor(public name: string, public age: number) {
        
    }
}

// 实例化对象p1
const p1 = new Person('孙悟空', 18)
console.log(p1) // Person { name: '孙悟空', age: 18 }
p1.name = '猪八戒'
p1.age = 28
console.log(p1) // Person { name: '猪八戒', age: 28 }
// 实例化对象p2
const p2 = new Person('猪八戒', 28)
console.log(p2) // Person { name: '猪八戒', age: 28 }
p2.name = '沙和尚'
p2.age = 38
console.log(p2) // Person { name: '沙和尚', age: 38 }

构造函数中this指向: 此时构造函数中的this指向的是通过类创建出的实例

class Person {
    // 定义实例属性类型
    name: string
    age: number
    // 定义构造函数
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
        console.log(this) // Person {name: "孙悟空", age: 18}
    }
}

// 实例化对象p1, 调用new Person('孙悟空', 18)时,console.log(this)中的this指向p1
// 此时打印出的this是Person {name: "孙悟空", age: 18}
const p1 = new Person('孙悟空', 18)

类的实例方法和this指向

实例方法中的this指向的是调用它的实例,谁调用了实例方法,this就指向谁

class Person {
    // 定义实例属性类型
    name: string
    age: number
    // 定义构造函数
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    // 定义实例方法
    sayHello() {
        console.log(this) // 调用该方法的实例对象
        console.log(this.name + 'hello')
    }
}
// 实例化对象
const p1 = new Person('孙悟空', 18)
const p2 = new Person('猪八戒', 28)

// 调用实例方法
p1.sayHello() // 孙悟空hello
p2.sayHello() // 猪八戒hello

类的静态方法和this指向

类定义静态方法后可以直接使用类名.方法名调用,此时静态方法中的this指向类本身,因此在方法内部无法获取实例属性的值,只能获取到静态属性

class Person {
    // 定义实例属性
    name: string = '孙悟空'
    // 定义静态属性
    static age: number = 18

    // 定义静态方法
    static sayHello() {
        console.log(this.age, this.name) // 18 undefined
    }
}

// 调用静态方法
Person.sayHello()

类的继承

类的继承是面向对象编程中的一个重要概念,它允许一个类从另一个类派生出来,并继承父类的属性和方法。在 TypeScript 中,类的继承通过使用 extends 关键字实现。

继承属性和方法
// 定义一个动物类
class Animal {
    name: string
    age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    eat() {
        console.log(this.name + '正在吃~~~')
    }
}

// 定义一个狗类
class Dog extends Animal {

}

// 定义一个猫类
class Cat extends Animal {

}

// 实例化一个狗类
let dog = new Dog('小黑', 3)
dog.eat() // 小黑正在吃~~~
// 实例化一个猫类
let cat = new Cat('小花', 2)
cat.eat() // 小花正在吃~~~
定义自己的方法,重写父类的方法

当我们继承了父类的属性和方法后,我们也可以在自己的类中定义自己的方法,并且可以重写父类继承过来的方法

// 定义一个动物类
class Animal {
    name: string
    age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    eat() {
        console.log(this.name + '正在吃~~~')
    }
}

// 定义一个狗类
class Dog extends Animal {
    // 定义自己的方法
    run() {
        console.log(this.name + '正在跑~~~')
    }
    // 重写父类的方法
    eat() {
        console.log(this.name + '正在吃狗粮~~~')
    }
}

// 定义一个猫类
class Cat extends Animal {
    // 定义自己的方法
    catchMouse() {
        console.log(this.name + '正在抓老鼠~~~')
    }
    // 重写父类的方法
    eat() {
        console.log(this.name + '正在吃猫粮~~~')
    }
}

// 实例化一个狗类
let dog = new Dog('小黑', 3)
dog.eat() // 小黑正在吃狗粮~~~
dog.run() // 小黑正在跑~~~
// 实例化一个猫类
let cat = new Cat('小花', 2)
cat.eat() // 小花正在吃猫粮~~~
cat.catchMouse() // 小花正在抓老鼠~~~
继承中的super关键字

super 关键字用于在子类中调用父类的构造函数、访问父类的属性和方法

// 定义一个动物类
class Animal {
    name: string
    age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    eat() {
        console.log(this.name + '正在吃~~~')
    }
}

// 定义一个狗类
class Dog extends Animal {
    // 定义自己的属性
    breed: string
    constructor(name: string, age: number, breed: string) {
        // 调用父类的构造函数
        super(name, age)
        this.breed = breed
    }
    // 定义自己的方法
    run() {
        // 调用父类的方法
        super.eat()
        console.log(this.breed + '正在跑~~~')
    }
}

// 实例化一个狗类
let dog = new Dog('小黑', 3, '哈士奇')
dog.run() // 哈士奇正在跑~~~  哈士奇正在吃~~~

抽象类和抽象方法

当我们如果创建了一个父类,用来被继承,但是父类本身也是个类,也可以创建自身的实例,此时我们又不想让父类可以创建自身的实例,我们就可以使用抽象类作为父类,抽象类无法创建实例,只能被继承。父类中的方法实际上意义不大,用的时候几乎都需要重写,因此我们可以在父类中不定义具体的方法体,只是定义个抽象方法,专门用来给子类重写,抽象方法只能定义在抽象类中。 抽象类和抽象方法都用abstract修饰

// 定义一个抽象动物类
abstract class Animal {
    // 定义属性
    name: string
    age: number
    // 定义构造函数
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    // 定义抽象方法
    abstract eat(): void
}

// 定义一个狗类
class Dog extends Animal {
    // 定义自己的属性
    breed: string
    constructor(name: string, age: number, breed: string) {
        // 调用父类的构造函数
        super(name, age)
        this.breed = breed
    }
    // 重写父类的抽象方法
    eat() {
        console.log(`${this.breed}正在吃~~~`)
    }
}

// 实例化一个狗类
let dog = new Dog('小黑', 3, '哈士奇')
dog.eat() // 哈士奇正在吃~~~

// 实例化动物类
// let animal = new Animal('小花', 2) // 报错,抽象类无法实例化

封装类的实例属性

我们如果把实例属性都定义为公共属性,当我们使用类实例化一个对象之后,实例化后的对象可以随意修改实例属性的值,如果一些值是不合法的我们也无法阻止,因此我们需要将属性设置成私有属性,然后通过实例方法来获取和设置该属性,我们可以在实例方法中处理异常逻辑

class Person {
    // 属性封装
    private _name: string
    private _age: number

    // 构造函数
    constructor(name: string, age: number) {
        this._name = name
        this._age = age
    }

    // 属性getter和setter
    get name(): string {
        return this._name
    }
    set name(name: string) {
        this._name = name
    }

    get age(): number {
        return this._age
    }
    set age(age: number) {
        // 判断年龄是否合法
        if (age < 0) {
            throw new Error('年龄不能小于0');
        }
        this._age = age
    }
}

const p = new Person('jack', 18)
console.log(p.name) // jack
console.log(p.age) // 18
p.name = 'rose'
p.age = 17
console.log(p.name) // rose
console.log(p.age) // 17
p.age = -1 // 报错:年龄不能小于0

接口

在 TypeScript 中,接口(Interface)用于描述对象的形状(Shape)。接口定义了对象应该具有的属性和方法,并可以用于类型检查和类型推断。定义接口时,使用关键字Interface

使用接口定义类型

接口可以和type一样用来定义变量的类型

interface Person {
    name: string,
    age: number,
    email?: string, // 可选属性
    add(x: number, y: number): number, // 定义方法
    subtract: (x: number, y: number) => number // 定义方法的另一种方式
    readonly x: number // 定义只读属性,只能在创建对象时赋值,不能被修改
}

let p1: Person = {
    name: '张三',
    age: 18,
    add: function (x: number, y: number): number {
        return x + y;
    },
    subtract: function (x: number, y: number): number {
        return x - y;
    },
    x: 10
}

接口的继承

接口和类有相似的继承机制,继承后的接口拥有父接口和自身接口属性的合集

// 定义一个父接口
interface Person {
    name: string,
    age: number,
    email?: string, // 可选属性
}

// 定义一个子接口,继承父接口
interface Student extends Person {
    add: (x: number, y: number) => number,
    subtract: (x: number, y: number) => number,
}

let p1: Student = {
    name: '张三',
    age: 18,
    add: function (x: number, y: number): number {
        return x + y;
    },
    subtract: function (x: number, y: number): number {
        return x - y;
    }
}

使用类实现接口

接口可以在定义类的时候限制类的类型,接口在某种意义上与抽象类类似,因此当用接口去限制类的类型的时候,我们也称之为使用该类实现这个接口

// 定义一个接口
interface IPerson {
    name: string
    age: number
    sayHello(): void
}

// 定义一个类
class Person implements IPerson {
    name: string
    age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
    sayHello() {
        console.log('大家好,我是' + this.name)
    }
}

泛型

在 TypeScript 中,泛型(Generics)是一种在定义函数、类、接口等可重用组件时,允许在声明的时候不指定具体类型,而是在使用时再指定类型的机制。使用泛型可以增加代码的灵活性和复用性,使代码更具通用性,同时保持类型安全。泛型的定义使用尖括号(<>)来表示,通常使用单个大写字母 T 表示泛型类型,当然也可以使用其他大写字母或描述性的名称。

定义泛型函数

当我们定义一个函数时,有时候我们并不知道传入的参数类型是什么,也不知道返回值的类型是什么,只是知道传入的参数类型和返回类型相同,这时候我们就可以使用泛型来定义函数,其中是我们定义的一个泛型,我们指定该函数的参数应该是T类型,返回值也应该是T类型,T可以表示任意类型

function identity<T>(arg: T): T {
    return arg
}

使用泛型函数

使用泛型函数时,我们可以指定T的类型也可以直接使用ts的类型推断能力,以下是两种方式

let output = identity<string>("myString")
// 或者
let output = identity("myString")

使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。例如我们在泛型函数中使用一些数组的性质

function identity<T>(arg: T): T {
    console.log(arg.length)  // Error: T doesn't have .length
    return arg
}

此时编辑器会报错,因为如果传入的类型是number类型的话,number是没有length属性的,我们可以做如下修改,此时指定我们的参数类型和返回值类型都是数组,但是数组内的类型是可以任意的,可以是string,number等

function identity<T>(arg: T[]): T[] {
    console.log(arg.length)  // Error: T doesn't have .length
    return arg
}
// 或者
function identity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length)  // Error: T doesn't have .length
    return arg
}

泛型函数的类型

当我们定义了一个泛型函数之后,我们又如何给这个泛型函数定义类型呢,其实泛型函数定义类型的方式和普通函数相同,例如下面我们就定义了一个普通函数类型和泛型函数类型

type functionType = {
    normalFunction: (value: string) => string,
    genericFunction: <T>(value: T) => T
}