TypeScript的扩展类型使用

139 阅读6分钟

为什么使用扩展类型

TS提供的类型无法满足开发需求,可以自己开发一些类型,就是扩展类型

常见扩展类型

类型别名、枚举、接口、类

枚举

枚举通常用来约束某个变量的取值范围(字面量配合联合类型也可以实现枚举的作用,能使用枚举尽量不适用字面量配合联合类型)

字面量存在的问题

type Gender = '男' | '女' 
// 1、若后续将性别修改为male、female、帅哥、美女、先生、女士,则使用字面量的话需要大量修改代码,不好维护
let gender: Gender
gender = '男'
gender = '女'
// 2、字面量在编译后的js代码中不会存在,js中若要读取某个变量的具体取值范围(性别有哪些),无法实现

枚举使用方法

enum Gender {
    male = '男', //写法 逻辑名称 = 真实值
    female = '女'
}
let gender: Gender
gender = Gender.male
gender = Gender.female
console.log(gender); // 女,枚举通过使用逻辑名称来获取真实值
// 枚举将逻辑名称和真实值分开了,不像字面量逻辑名称和具体值都是相同的,解决了维护不方便的问题

枚举会参与具体的编译

上述ts代码编辑后结果如下,实际是编译成了对象

var Gender;
(function (Gender) {
    Gender["male"] = "\u7537";
    Gender["female"] = "\u5973";
})(Gender || (Gender = {}));
let gender;
gender = Gender.male;
gender = Gender.female;
console.log(gender);

数字枚举

如果枚举不赋值,默认为从0开始的数字枚举,并且数字枚举会自增

enum Level {
    level1, // 相当于level1 = 0
    level2, // 相当于level2 = 1
    level3, // 相当于level3 = 2
}

let level: Level = Level.level1
level = Level.level2
level = 2 // 只有数字枚举可以直接赋值,字符串枚举不可以
console.log(level); // 2

数字枚举编译后和字符串枚举不一样

enum Level {
    level1,
    level2,
    level3
}
function printLevel(level: Level) {
    console.log(level);
}
printLevel(Level.level1) // 虽然数字枚举可以直接printLevel(1),但是不推荐直接用枚举值,而是通过枚举定义实现

编译后结果

var Level;
(function (Level) {
    Level[Level["level1"] = 0] = "level1";
    Level[Level["level2"] = 1] = "level2";
    Level[Level["level3"] = 2] = "level3";
})(Level || (Level = {}));
/**
上述代码相当于
Level = {
    level1 : 0,
    level2 : 1,
    level3 : 2,
    0: 'level1',
    1: 'level2',
    2: 'level3'
}
*/
function printLevel(level) {
    console.log(level);
}
printLevel(Level.level1);

接口

TypeScript中接口是一种用于约束类、对象、函数的标准。类比于电源接口、usb接口其实都是一些标准。 制定标准的形式:

  • 文档标准,如联调的接口文档,弱标准
  • 代码约束,如接口interface,强标准,会提示错误

接口约束对象

interface User {
    // 写法类似类型别名,如果只是约束对象,接口和类型别名都可以使用,但是推荐使用接口形式
    name: string  
    age: number
}
type People = {
    name: string
    age: number
}
let user: User = {
    name: 'zs',
    age: 11
}

接口约束函数

约束对象里面的成员函数

接口和类型别名编译后都不会存在于js文件中

interface User {
    name: string
    age: number
    // sayHi: ()=>void
    sayHi():void // 表示没有参数也没有返回值的函数,也可以用上面注释的方法
}
type People = {
    name: string
    age: number
    sayHi:()=>void
    // sayHi():void
}
let user: User = {
    name: 'zs',
    age: 11,
    // sayHi: ()=>{}
    sayHi(){}
}

约束普通函数

// 提取为类型别名
// type Condition = (num: number) => boolean
// 提取为类型别名
interface Condition {
    // 大括号里面没有成员名称,不表示一个对象,只是一个定界符,里面是约束内容,就是接口限制函数的写法
    (num: number): boolean
}
function sum(numbers: number[], callBack: Condition) {
    let s = 0
    numbers.forEach(n => {
        if (callBack(n)) {
            s += n
        }
    })
    return s
}
// 求所有奇数的和
let odds = sum([1, 2, 3, 4, 5, 6], n => n % 2 !== 0)
// 求所有偶数的和
let evens = sum([1, 2, 3, 4, 5, 6], n => n % 2 === 0)
console.log('odds:', odds); // 9
console.log('evens:', evens); // 12

接口的继承性

通过接口的继承性可以进行多个接口标准的组合,通过extends实现继承,比类型别名通过交叉类型实现更加方便

interface A {
    name: string
}
interface B extends A {
    age: number
}
interface C extends A,B {
    male: boolean
}
let user: C = {
    name: 'zs',
    age: 12,
    male: false
}

交叉类型实现上述效果

type A = {
    name: string
}
type B = {
    age: number
}
type C = {
    male: boolean
} & A & B // 通过'&'实现交叉类型
let user: C = {
    name: 'zs',
    age: 12,
    male: false
}

区别在于:

  1. 子接口,不能再次定义父接口中已经声明的成员,会报错
  2. 交叉类型相同的成员,会进行类型交叉,即同时具备多个类型

image.png

类的使用

类的属性初始化

构造函数初始化

ts中类的定义和js是有区别的,tsconfig中设置"strictPropertyInitialization": true,则属性必须初始化

// js中这段代码是正确的,但是ts中,会报错,ts不允许动态增加属性
class User {
    constructor(name, age){
        this.name = name
        this.age = age
    }
}
const obj = {}
obj.color = 'blue' // js中不会报错,ts中会报错,ts认为创建对象的时候对象的属性应该是确定的,不能动态添加属性,容易造成隐患,同时不便于维护

因此需要使用属性列表形式定义

class User {
    name: string  // 编译为js后不存在这行代码
    age: number  // 编译为js后不存在这行代码
    constructor(name: string, age: number){
        this.name = name
        this.age = age
    }
}

属性列表默认值初始化

image.png

可选属性

class User {
    name: string
    age: number
    // 使用属性默认值进行初始化,
    //必须设置默认值,才能不通过构造函数进行初始化,
    //转换成js还是通过构造函数进行初始化的
    gender: '男' | '女' = '男' 
    pid?: string // 表示pid属性可以没有,不用进行初始化
    constructor(name: string, age: number){
        this.name = name
        this.age = age
    }
}
const user = new User('zs', 18)
user.gender = '女'
console.log(user.gender);

不可更改属性

class User {
    readonly id: number // id属性不允许修改,使用readonly修饰来实现
    name: string
    age: number
    gender: '男' | '女' = '男' 
    pid?: string // 表示pid属性可以没有,不用进行初始化
    constructor(name: string, age: number){
        this.id = Math.random()
        this.name = name
        this.age = age
    }
}
const user = new User('zs', 18)
user.id = 123 // 报错

访问修饰符

  • public 类里面的成员(属性或者方法)默认的修饰符,可以省略不写,外部可以访问
  • private 只能在类的内部使用,类生成的对象无法外部直接访问
  • protected 受保护的成员,只能该类和该类的子类使用

如果构造函数传递的参数只做属性的赋值操作,通过修饰符简化初始化操作

class User {
    readonly id: number // id属性不允许修改,使用readonly修饰来实现
    gender: '男' | '女' = '男' 
    pid?: string // 表示pid属性可以没有,不用进行初始化
    constructor(public name: string, public age: number){
        this.id = Math.random()
    }
}
const user = new User('zs', 18)

访问器属性

访问器属性,有点类似vue的计算属性,用于给属性进行一些操作和限制

class User {
    gender: '男' | '女' = '男' 
    constructor(public name: string, private _age: number){
    }
    set age(value: number){
        // 生成器-设置器
        if (value <= 0) {
            this._age = 0 // 注意通常生成器都是操作私有成员的
        }else if (value >= 200) {
            this._age = 200
        }else {
            this._age = value
        }
    }
    get age(){
        // 生成器-读取器
        return Math.floor(this._age)
    }
}
const user = new User('zs', 18)
user.age = 200.5 // 设置年龄,对年龄通过设置器函数进行限制,通过生成器操作私有成员
console.log(user.age); // 200 读取年龄,对返回年龄格式通过读取器进行限制