接口补充、泛型、工具类型、空安全、模块化
今日核心:
- 接口、泛型、工具类型、空安全、模块化 相关资源:
- 图片素材:无
1. 接口补充
接口可以用来定义类型,也有一些其他的用法,咱们来看看
1.1. 接口继承
接口继承使用的关键字是 extends
interface 接口1{
属性1:类型
}
interface 接口2 extends 接口1{
属性2:类型
}
试一试:
- 定义接口 1
- 定义接口 2 继承接口 2
- 使用接口 2 进行类型限制
interface IFood {
name: string
}
interface IVegetable extends IFood {
color: string
}const flower: IVegetable ={
name:'西兰花',
color:'黄绿色'}
1.2. 接口实现
可以通过接口结合 implements 来限制 类 必须要有某些属性和方法,
interface 接口{
属性:类型
方法:方法类型
}
class 类 implements 接口{// 必须实现 接口中定义的 属性、方法,否则会报错}
试一试:
- 定义接口 a. 定义属性、方法
- 定义类 a. 实现接口 b. 额外添加 属性 、方法
- 思考: a. 昨天的作业,如何使用接口实现来优化
interface IDog {
name: string
bark:()=>void}
class Dog implements IDog {
name: string =''
food: string =''bark(){}}
2. 泛型
泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用
通俗一点就是:类型是可变的!
目前的学习重点:
- 理解泛型的语法,能够看明白系统中使用泛型的位置
- 泛型的封装会在项目中逐步增加
为了更好的理解为什么需要 类型可变,咱们来一个具体的例子:一个恒等函数,无论传入什么,直接返回
// 1. 字符串
function identityStr(arg: string): string {return arg
}// 2. 数字
function identityNum(arg: number): number {return arg
}// 3. 布尔
function identityBoolean(arg: boolean): boolean {return arg
}// 如果还要考虑其他类型的参数,那么就需要写很多个类似的函数,逻辑一样,区别就是类型不同// 如果类型可变就好啦~
2.1. 泛型函数
顾名思义就是,泛型和函数结合到一起使用
Type 是泛型参数的名字,类似于之前的形参,
● 可以自行定义,有意义即可
● 首字母大写
● 参见泛型参数名 T、Type
function 函数名<Type>(形参:Type):Type{}// 定义泛型参数 Type,后续可以使用类型的位置均可以使用比如:// 1. 参数类型
function 函数名<Type>(形参:Type){}// 2. 返回值类型
function 函数名<Type>(形参:Type):Type{}
使用泛型改写上一节的代码
// 函数定义
function identity<Type>(arg: Type): Type {return arg;}// 在使用的时候传入具体的类型,函数就可以正常工作啦~
identity<string>('123')
identity<number>(123)
identity<boolean>(false)
identity<string[]>(['1','2','3'])
结合 编译器 的 类型推断 功能,在使用函数的时候还可以进行简写,比如下面的写法,和上面的是一样的。
虽然大部分时候可以推断出类型,但是如果碰到 编译器 无法推断类型时,就需要显式传入类型参数,这在更复杂的示例中可能会发生。
identity('123')identity(123)identity(false)identity(['1','2','3'])
2.2. 使用泛型变量
类型变量可以用在任意支持的位置,实现更为复杂的功能,咱们来看 几 个具体的例子
2.2.1. 返回数组长度
定义函数 :参数是数组,但是数组项的 类型 需要动态设置,数组的长度作为返回值
function getLength<T>(...):number{return...}// 实现之后的代码支持如下调用
getLength<string>(['西兰花','花菜','秋刀鱼'])// 3
getLength<number>([1,2,3])// 3
将类型变量 Type,作为数组项的类型即可
function identity<Type>(arr: Type[]): number {return arr.length
}
2.2.2. 返回数组最后一位元素
一个数组恒等函数,无论传入类型的数组,直接返回数组最后一位
function arrIdentity<Type>(...)...{// ...}// 实现之后的代码支持如下调用
arrIdentity<string>(['西兰花','花菜','秋刀鱼'])// 返回 秋刀鱼
arrIdentity<number>([1,2,3])// 返回原数组 3
参考代码
function arrIdentity<Type>(arr: Type[]): Type {return arr[arr.length-1]}
2.3. 泛型约束
如果开发中不希望任意的类型都可以传递给 类型参数 ,就可以通过泛型约束来完成
核心步骤:
- 定义用来约束的 接口(interface)
- 类型参数通过 extends 即可实现约束
interface 接口{
属性:类型
}
function 函数<Type extends 接口>(){}// 后续使用函数时,传入的类型必须要有 接口中的属性
试一试1:
● 实现泛型函数,内部打印 传入 【参数的 length】 属性
● 通过泛型约束,保证传入的参数 【length 属性】
试一试 2:
● 实现泛型函数,内部打印 传入 【参数的 food】 属性
● 通过泛型约束,保证传入的参数 【food 属性】
interface ILengthwise {
length: number
}
function identity<Type extends ILengthwise>(arr: Type){
console.log(arr.length.toString())}// 使用的时候 只要有 length 属性即可identity([1,2,3,4])// 数组有 length 属性 正常运行identity('1234')// 字符串也有 length 属性 正常运行// identity(124) // 数值没有 length 属性 报错
class Cloth implements ILengthwise {
length: number =10}
class Trousers {
length: number =110}identity(new Cloth())// Cloth 有 length 属性 正常运行identity(new Trousers())// Trousers 有 length 属性 正常运行
interface IFood {
food: string
}
function logFood<T extends IFood>(param: T){
console.log('吃的挺好:', param.food)}
class Lunch {
food: string
constructor(food: string){
this.food = food
}}const l = new Lunch('醉面')logFood(l)
如果要加更多的约束,只需要给作为约束的接口 添加更多的内容即可。
2.4. 多个泛型参数
日常开发的时候,如果有需要可以添加多个 类型变量,只需要定义并使用 多个类型变量即可
function funcA<T, T2>(param1: T, param2: T2){
console.log('参数 1', param1)
console.log('参数 2', param2)}
funcA<string, number>('西兰花',998)
funcA<string[], boolean[]>(['西兰花'],[false])
根据需求,可以添加任意个类型变量,添加约束的方式也和之前的章节一致
2.5. 泛型接口
定义接口时结合泛型,那么这个接口就是 泛型接口
interface 接口<Type>{// 内部使用Type}
试一试:
- 定义泛型接口,接受泛型参数
- 接口定义 a. ids: 数组,类型使用 泛型参数 替代 b. getIds:函数,返回数组,参数类型和返回值类型 使用泛型参数替代
- 跳转到【 Array 】的定义
interface IdFunc<Type>{
id:(value: Type)=> Type
ids:()=> Type[]}
let obj: IdFunc<number>={id(value){return value },ids(){return[1,3,5]}}
2.6. 泛型类
和泛型接口类似,如果定义类的时候结合泛型,那么这个类就是 泛型类
class 类名<Type>{// 内部可以使用 Type}
试一试
- 定义泛型类,接收泛型参数 T
- 实现: a. 定义属性,类型为 T b. 构造函数,接收属性,类型为T c. 方法,返回属性,类型为T
// 定义
class Person <T>{
id: T
constructor(id: T){
this.id = id
}getId(): T {return this.id
}}// 使用
let p = new Person<number>(10)
3. 工具类型
ArkTS提供了4 中工具类型,来帮组我们简化编码
传送门
4 种工具类型中,熟练掌握 Partial 即可,可以简化编码
3.1. Partial
基于传入的Type类型构造一个【新类型】,将Type的所有属性设置为可选。
type 新类型 = Partial<接口>
type 新类型 = Partial<类>// 后续使用新类型即可
试一试:
- 定义 接口,添加属性
- 通过 Partial 基于上一步定义的类,返回新类型
- 使用 新类型,确认结果
- 调整【基础代码】,结合Partial简化默认值的设置
interface Person {
name: string
age: number
friends: string[]}
type ParPerson = Partial<Person>// 因为都是可选的,可以设置为空对象
let p: ParPerson ={}
基础模版
interface Food{
name:string
color:string
price:number
materials:string[]}
@Entry
@Component
structPage02_toolType{
@State message: string ='工具类型';
food:Food ={}build(){Row(){Column(){Text(this.message).fontSize(50).fontWeight(FontWeight.Bold)}.width('100%')}.height('100%')}}
参考代码:
interface Food{
name:string
color:string
price:number
materials:string[]}
@Entry
@Component
structPage02_toolType{
@State message: string ='工具类型';// 使用 Partial 将 Food 的每一项转为可选,就不需要都设置默认值啦
food:Partial<Food>={}build(){Row(){Column(){Text(this.message).fontSize(50).fontWeight(FontWeight.Bold)}.width('100%')}.height('100%')}}
3.2. Required
基于传入的Type类型构造一个【新类型】,将 Type 的所有属性设置为必填
type 新类型 = Required<接口>
type 新类型 = Required<类>// 后续使用新类型即可
试一试:
- 定义 类,添加属性,全部设置为可选
- 通过 Required 基于上一步定义的类,返回新类型
- 使用 新类型,确认是否转为必填属性
class Person {
name?: string
age?: number
friends?: string[]}
type RequiredPerson = Required<Person>// 都是必须得属性,必须设置值
let p: Required<Person>={
name:'jack',
age:10,
friends:[]}
3.3. Readonly
基于 Type构造一个【新类型】,并将Type 的所有属性设置为readonly
type 新类型 = Readonly<接口>
type 新类型 = Readonly<类>// 后续使用新类型即可
试一试:
- 定义 类,添加属性
- 通过 Readonly 基于上一步定义的类,返回新类型
- 使用 新类型,确认结果
class Person {
name: string =''
age: number =0}
type ReadonlyIPerson = Readonly<Person>const p: ReadonlyIPerson ={
name:'jack',
age:10}
p.name ='rose'// 报错 属性全部变成只读
3.4. Record<Keys,Type>
构造一个对象类型,其属性键为Keys,属性值为Type。该实用程序可用于将一种类型的属性映射到另一种类型。
class CatInfo {
age: number =0
breed: string =''}// 联合类型
type CatName ="miffy"|"boris"|"mordred";// 通过Record 构建新的对象类型// 属性名:必须是CatName 中的值// 属性值:必须是CatInfo 类型
type Ctype = Record<CatName, CatInfo>const cats: Ctype ={'miffy':{ age:5, breed:"Maine Coon"},'boris':{ age:5, breed:"Maine Coon"},'mordred':{ age:16, breed:"British Shorthair"},};
试一试:
- 运行上述代码尝试通过 cats 获取内部的 CatInfo 对象
- 思考:如何添加键的数量?
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// 取值的时候,根据需求可能需要考虑 屏蔽掉空值的情况if(x != null){/* do something */}
@Entry
@Component
structIndex{
@State count: number | null =0build(){Column(){Button('++').onClick(()=>{
this.count++// 会报错})}}}
4.2. 非空断言运算符
后缀运算符! 可用于断言其操作数为非空。
应用于空值时,运算符将抛出错误。否则,值的类型将从T | null更改为T:
let x: number | null =1;
let y: number
y = x +1;// 编译时错误:无法对可空值作加法
y = x!+1;// 通过非空断言,告诉编译器 x不为 null
4.3. 空值合并运算符
空值合并二元运算符?? 用于检查左侧表达式的求值是否等于null。如果是,则表达式的结果为右侧表达式;否则,结果为左侧表达式。
换句话说,a ?? b等价于三元运算符a != null ? a : b。
在以下示例中,getNick方法如果设置了昵称,则返回昵称;否则,返回空字符串:
class Person {
name: string | null = null
getName(): string {// return this.name === null ? '' : this.name// 等同于 如果 name不为空 就返回 name 反之返回 ''return this.name ??''}}
4.4. 可选链
在访问对象属性时,如果该属性是undefined或者null,可选链运算符会返回undefined。
class Dog {bark(){
console.log('啊呜~')}}
class Person {
name?: string
dog?: Dog
constructor(name: string, dog?: Dog){
this.name = name
this.dog = dog
}}const p: Person = new Person('jack')// p.dog.bark()// 报错 dog 可能为空// 逻辑判断if(p.dog){
p.dog.bark()}// 当 dog不为null 或 undefined 时 再调用 bark 并且不会报错
p.dog?.bark()
5. 模块化
接下来咱们开始学习在日常开发中非常重要的一个概念-模块化
概念:
- 把一个大的程序拆分成【互相依赖】的若干小文件
- 这些小文件还可以通过【特定的语法】组合到一起
- 这个过程称之为【模块化】
优点:
a. 更好维护
b. 更好的复用性
缺点:
c. 需要学习模块化语法
分析:
- 功能写完只有10行代码,模块化没啥必要!
- 功能写完有100行,或者1000行代码,里面有【好几段逻辑】在其他地方也要用–模块化
逻辑都写在一起,并且越写越多------------------模块化之后 (😄)
5.1. 默认导出和导入
ArkTS 中每个 ets 文件都可以看做是一个模块,默认只能在当前文件中使用,如果要在其他位置中使用就需要:
- 当前模块中 导出模块
- 需要使用的地方 导入模块
// 默认导出
export default 需要导出的内容
// 默认导入
import xxx from '模块路径'
试一试:
- ets 目录下创建【目录 models】
- models 目录下【创建 model1.ets】 a. 定义类 ⅰ. 属性 ⅱ. 构造函数,初始化属性 ⅲ. 方法,内部打印属性 b. 使用【默认导出】语法,将其导出
- 页面中【导入 model1.ets 中导出的内容】 并使用
示例代码:
// Model.ets// 一起写
export default class Person {
name: string =''}// 分开写// export default Person// 只能有一个默认导出// TestModel.ets
import Person from './model'const p: Person = new Person()
5.2. 相对路径
学习了模块拆分之后,就需要开始导入文件啦,这个时候就会用到相对路径,咱们来看看相关的概念
路径:查找文件时,从起点到终点的路线
路径分类:
- 相对路径:从当前文件出发去查找目标文件
- 绝对路径:从指定盘符触发去查找目标文件
/ 表示进入某个文件夹里面
. 表示当前文件所在文件夹 ./.. 表示当前文件的上一级文件夹 ../
5.3. 按需导出和导入
如果有很多的东西需要导出,可以使用按需导出,他也有配套的导入语法
// 逐个导出单个特性
export let name1, name2, …, nameN;// also var, const
export let name1 = …, name2 = …, …, nameN;// also var, const
export function FunctionName(){...}
export class ClassName {...}// 一次性导出
export { name1, name2, …, nameN };// --------------------------------// 导入
import { export1, export2, export3 as 别名 } from "module-name";
试一试:
- 在 models目录下创建 【model2.ets】
- model2.ets中定义 a. 常量 b. 函数 c. 类 d. 接口 e. 使用 【按需导出语法】 导出
- 在页面中,导入并使用
// ---------- Model.ets ----------const info: string ='信息'// 单独写
export const num: number =10
function sayHi(){
console.log('你好吗~')}// 或者 写到 {} 内部
export {
info, sayHi
}// ---------- MainFile.ets ----------
import { info, num, sayHi as sayHello } from './Model'
console.log('info:', info)// 起别名sayHello()
console.log('num:', num +'')
5.4. 全部导入
导出部分不需要调整,调整导入的语法即可
import * as Utils from './utils'// 通过 Utils 即可获取 utils模块中导出的所有内容
6. 案例-汽车店描述:
接下来通过一个具体的案例来巩固今天学习的语法
需求1:
定义接口 Vehicle(交通工具),记录交通工具的【通用信息】
● 属性:
○ name:字符串类型,名字
○ price:数值类型,价格
○ desc:字符串类型,描述
定义类 Truck(货车),实现 Vehicle 接口
● 属性:
○ load:数值类型,载重
● 构造函数:依次传入属性进行初始化
定义类 Car(轿车),实现 Vehicle 接口
● 属性:
○ color:字符串类型,颜色
○ capacity:数值类型,载客数
定义泛型类 CarShop, 使用 Vehicle 进行泛型约束,泛型参数 T
● 属性
○ name:字符串名字,商店名
○ vehicles: T 数组。使用泛型参数约束的数组
● 构造函数:传入名字进行初始化
● 方法:
○ addVehicle(vehicle:T){}.添加交通工具
■ 参数类型为泛型参数 T,无返回值
■ 传入 Truck 或者 Car 实例,内部添加到 数组 vehicles 中
○ showVehicle(){}.展示交通工具
■ 无参数,无返回值
■ 调用时依次打印 vehicles 数组中的 通用信息
需求 2:
将上述内容 编写到 单独的文件中,暴露 Truck,Car,CarShop。然后测试使用
参考代码:
// 定义接口 Vehicle(交通工具)
interface Vehicle {
name: string;
price: number;
desc: string;}// 定义类 Truck(货车),实现 Vehicle 接口
class Truck implements Vehicle {
name: string;
price: number;
desc: string;
load: number;constructor(name: string, price: number, desc: string, load: number){
this.name = name;
this.price = price;
this.desc = desc;
this.load = load;}}// 定义类 Car(轿车),实现 Vehicle 接口
class Car implements Vehicle {
name: string;
price: number;
desc: string;
color: string;
capacity: number;constructor(name: string, price: number, desc: string, color: string, capacity: number){
this.name = name;
this.price = price;
this.desc = desc;
this.color = color;
this.capacity = capacity;}}// 定义泛型类 CarShop,使用 Vehicle 进行泛型约束,泛型参数 T
class CarShop<T extends Vehicle>{
name: string;
vehicles: T[];constructor(name: string){
this.name = name;
this.vehicles =[];}// 添加交通工具方法addVehicle(vehicle: T):void{
this.vehicles.push(vehicle);}// 展示交通工具方法showVehicles():void{for(const vehicle of this.vehicles){
console.log(`Name: ${vehicle.name}`);
console.log(`Price: ${vehicle.price}`);
console.log(`Description: ${vehicle.desc}`);}}}
export {Truck,Car,CarShop}
7. 补充:从TypeScript到ArkTS的适配规
对于有 TS 基础的小伙伴可以参考一下如下的文档,因为并不是所有的 TS 语法都可以在 ArkTS 中使用
版权归原作者 一鲸落可万物生 所有, 如有侵权,请联系我们删除。