接口补充、泛型、工具类型、空安全

129 阅读8分钟

1. 接口补充

接口可以用来定义类型,也有一些其他的用法。

1. 1 接口继承

接口继承关键字为extends

基本代码为:

interface 接口1{
  属性1:类型
}
interface 接口2 extends 接口1 {
  属性2:类型
}

举例:

/*
 * 接口继承:
 * 接口1 extends 接口2
 * 注意点:
 * 1. 接口1中属性如果在接口2中也有同名同类型的,则自动合并
 * */

interface iPerson {
  name: string
}

interface iStudent extends iPerson {
  // name1:boolean
  age:number
}

let stu: iStudent = {
  name: '张三',
  age: 0
}

1.2 接口实现(类来实现接口)

可以通过接口结合 implements 来限制 类 必须要有某些属性和方法

基本代码:

interface 接口{
  属性:类型
  方法:方法类型
}

class 类 implements 接口{
  // 必须实现 接口中定义的 属性、方法,否则会报错
}

举例:

/*
 * 一个类要实现接口
 * 作用:接口是用来约束类定义的最小单元
 * 语法结构:1.准备一个接口,定义相关的单元(属性,方法)
 * 2. 准备一个类来实现这个接口的最小单元
 *
 * 注意点:类可以扩展自己的属性,方法
 * */

interface IDog {
  name: string
  bark: () => void // 约定狗叫方法是没有参数没有返回值的
}

class Dog implements IDog {
  name: string = ''
  age: number = 1

  bark() {
    console.log('汪汪')
  }

  eat() {
    console.log('吃')
  }
}

// 使用:
// 两种写法区别
// 1. 使用类作为对象的类型,可以调用类中的所有属性和方法
let dog:Dog = new Dog()
dog.eat()//吃
dog1.bark()//汪汪
// 2. 使用接口作为对象的类型,可以调用类中实现了接口中的属性和方法
let dog1:IDog = new Dog()//因为dog1类型为IDog,而IDog是用classDog类来实现,所以这里也需要进行new
dog1.bark()//汪汪

2.泛型

泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用,通俗一点就是: 类型 是可变的!

// 1. 字符串
function getStr(arg: string) {
  return [arg]  // 返回 string[]类型数组
}

// 2. 数字
function getNum(arg: number) {
  return [arg] // 返回 number[]类型数组
}

// 3. 布尔
function getBoolean(arg: boolean) {
  return [arg] // 返回 boolean[]类型数组
}

2.1 泛型函数

基本结构

// 定义泛型参数 Type,后续可以使用类型的位置均可以使用比如: 形参、函数内部、返回值

// 1. 参数类型
function 函数名<Type>(形参:Type){
}

// 2. 返回值类型
function 函数名<Type>(形参:Type):Type{
}

举例:

/*
 * 泛型函数演示:
 * 1. 泛型函数语法:
 *    function 函数名称<T>(形参:T) {
 *      let tmp:T = 形参
 *      return [形参]   // T[]
 * }
 *
 * ✨✨总结:泛型函数的使用步骤
 * 1. 泛型函数定义(类型使用T来占位)
 * 2. 泛型函数调用(传入具体类型)
 * */

// 需求:传入一个参数,返回这个参数的数组形式
// 1. 定义泛型函数
function getDataList<T>(args: T) {
  return [args] //返回T[]
}

// 2. 调用泛型函数,注意:要传入具体类型
let nums = getDataList<number>(100)
console.log('',nums)

//使用
getDataList<string>('100')
getDataList<boolean>(true)
getDataList<string[]>(['100','200'])

说明:当定义函数的时候不知道将来会传入一个什么类型的数据时,就可以使用泛型函数。

2.2 泛型约束

如果开发中不希望任意的类型都可以传递给 类型参数 , 就可以通过泛型约束来完成

2.2.1 联合类型的举例

// 泛型函数getData的T约束只能传入数字和字符串这两个类型的参数
function getData<T extends number | string>(args:T){
  return [args]
}

getData('ok') // ✔️
getData(100) // ✔️
getData(true) // ❌

说明:其中的number | string也可以改为: type 别名 = number | string

2.2.2 枚举类型约束

// 约束T只能是Color枚举中的一个值
function getColor<T extends Color>(color: T) {
  return color
}

getColor(Color.White)// ✔️
getColor('White')// ❌

2.2.3 接口类型的约束

function getDataInterface<T extends iPerson>(args:T){
  return args
}
getDataInterface<iPerson>({name:'明明',age:20})//✔️
// getDataInterface<iPerson>({})//❌

说明:接口类型的约束必须严格按照接口的类型来,除非接口类型里面有可选参数,不然的话必须严格传参数。

2.3. 多个泛型参数

举例:

function funA<T1 extends string, T2 extends number>(a1: T1, a2: T2) {
  console.log('a1=', a1, 'a2=', a2)
}

funA<string, number>('明明', 22)//✔️
// funA<string, boolean>('明明', true)  //❌

说明:extends后面跟的类型可以是基本类型如number,string等等,也可以是复杂类型数组等等,也可以是联合类型,枚举类型

2.3.1 例题

例题:

  • ① 需要对泛型参数进行类型约定为: string | number
  • ② 需要在方法体内部判断形参类型判断 typeof 形参
  • ③ 根据形参类型来决定是返回字符串还是返回数字

参考答案

// 1. 定义泛型函数
function getRandomHW<T extends string | number>(args?: T) {
  //  1.1 生成一个100以内的随机数
  let rdNum = Math.floor(Math.random() * 101)
  // return `${rdNum}%`
  // return rdNum

  //   1.2 判断返回的是字符串还是数字?
  //   typeof args 获得args这个形参的类型,再与string,number来比较
  if (typeof args == 'string') {
    return `${rdNum}%`
  } else {
    return rdNum
  }
}

// 2. 调用函数
// let res = getRandomHW<string>()
// // console.log(res.toString())
//
// let res1 = getRandomHW<number>()
// console.log(res1.toString())

@Entry
@Component
struct Index {
  build() {
    Column() {

    }
    //3.UI界面上使用
    // .height(getRandomHW<string>())
    .height(getRandomHW<number>())
    .width('100%')
    .backgroundColor(Color.Pink)
  }
}

说明:在定义泛型函数时,在形参加上一个?号变成一个可选参数,就可以在height(getRandomHW/number/())的括号里不用传入参数,因为number以及指定了是一个数据类型的参数

2.4 泛型接口

例如:

interface iData<T> {
 code: number,
 msg: string,
 data: T
}

let obj1: iData<string[]> = {
 code: 200,
 msg: 'ok',
 //data:[1,2]  // ❌
 data:['1','2']  // ✔️
}

let obj2: iData<number[]> = {
 code: 200,
 msg: 'ok',
 data:[1,2]  // ✔️
 //data:['1','2']  // ❌
}

// 要使用JSON.stringify才能正常打印出数组类型和对象类型的具体内容
console.log('',JSON.stringify(data1))

说明:如果在创建一个对象接口时不知道将来里面参数的类型,则就可以使用泛型接口进行创建,然后当在定义的对象的时候在明确类型就可以了。

2.5 泛型类

泛型类跟泛型接口类似,当在创建一个类时不知道将来会传入什么类型的数据,则可以使用泛型类进行先占位。 举例:

class Person<T> {
  id: T

  constructor(id: T) {
    this.id = id
  }

  sayHi() {
  //这里需要使用this进行调用id,因为以及实例化了
    return this.id
  }
}
//跟接口不同的是在进行引用时要进行new实例化
let obj = new Person<string>('hello')
let obj1 = new Person<number>(100)

3.工具类型

ArkTS提供了4 中工具类型,来帮助我们简化编码

3.1. Partial

基于传入的Type类型构造一个【新类型】,将Type的所有属性设置为可选。

举例:

class Person {
  name: string = ''
  age: number = 0
  friends: string[] = []
}

type ParPerson = Partial<Person>

// 因为都是可选的,可以设置为空对象
let p: ParPerson = {}

说明:原本类Person里面的参数为必选参数,在进行Partial后,ParPerson里面的参数就变为了可选参数。

3.2.Required

基于传入的Type类型构造一个【新类型】,将 Type 的所有属性设置为必填,它跟Partial作用相反。

class Person {
  name?: string
  age?: number
  friends?: string[]
}

type RequiredPerson = Required<Person>

//原来都为可选参数,现在都是必须属性,必须设置值
let p: Required<Person> = {
  name: 'jack',
  age: 10,
  friends: []
}

说明:作用跟Required相反,原本类Person里面的参数为可选参数,在进行Required后,p里面的参数就变成了必选参数了。

3.3.Readonly

基于 Type构造一个【新类型】,并将Type 的所有属性设置为readonly

class Person {
  name: string = ''
  age: number = 0
}

type ReadonlyIPerson = Readonly<Person>

let newp: ReadonlyIPerson = {
  name: 'jack',
  age: 10
}

newp.name = 'rose' // 报错 属性全部变成只读

说明:原本定义好了一个类,然后用type给原本的类取了一个别名,在给这个新对象进行赋值,但是在进行改name的时候会报错,因为这个时候ReadonlyIPerson里面的参数全部改为仅可读。

3.4.Record

构造一个对象类型,其属性键为Keys,属性值为Type。该实用程序可用于将一种类型的属性映射到另一种类型。

/*
 * Record<Key,Value>
 *   作用:可以用来修饰一个对象的key和value的类型,将来可以省略interface定义来直接使用变量存储一个对象
 *   语法: Record<key,value>  -> key表示对象的属性类型,value表示的对象的属性值的类型
 *
 * 注意:取值的时候,使用 对象名称['属性名'] -> 属性值
 * */
 
 //场景:
@State person = {'name':'张三'}  // ❌这样定义报错 -> 因为没有使用interface指定对象类型


//所以可以这样不用定义interface,快速进行读取值
let obj:Record<string,string> = {'name'  :  '张三'}
let nameStr =  obj['name']  
console.log(nameStr)//  张三

let obj1:Record<string,number> = {'age':20}
let age = obj1['age'] 
console.log(age)//  20

说明:如果不想定义interface,就可以直接使用Record<Key,Value>来指定对象类型即可

4. 空安全

默认情况下,ArkTS中的所有类型都是不可为空的。如果要设置为空,需要进行特殊的处理,并且在获取 可能为空的值的时候也需要特殊处理。

4.1. 联合类型设置为空

举例:

//场景:
let x: number = null    // 编译时错误
let y: string = null    // 编译时错误
let z: number[] = null  // 编译时错误



// 通过联合类型设置为空
let x: number | null = null
x = 1    // ok
x = null // ok

4.2. 非空断言运算符

举例:

let x: number | null
let y: number
y = x + 1;  // 编译时错误:无法对可空值作加法
y = x! + 1; // 通过非空断言,告诉编译器 x不为 null

说明:在变量后面加!叫做非空断言。

4.3. 空值合并运算符

举例:

class Person {
  name: string | null = null

  getName(): string {
    //三元表达式
    // return this.name != null ? this.name : '' 
    
    
    // 上面的三元表达式等同于 如果 name不为空 就返回 name 反之返回 ''
    return this.name ?? ''
  }
}

说明:a ?? b等价于三元运算符a != null ? a : b

4.4. 可选链

举例:

interface iPerson{
  name:string
  dogAge?:number  // 人有宠物狗也有可能没有宠物狗,所以狗的年龄dogAge为可选
}

 //定义人对象
let person:iPerson = {name:'明明'} 


// 打印人拥有的宠物狗年龄
console.log(person.dogAge.toString())  // 报错❌



// 解决方案1:使用if判断来判断如果为空就不执行,编译器认为这个代码安全,运行执行✔️
if(person.dogAge){
  console.log(person.dogAge.toString())
}


// 解决方案2:使用?可选链来断定可能为空,编译器认为这个代码安全,运行执行✔️
console.log(person.dogAge?.toString())