07 ArkTS 基础类型与接口定义:从 TypeScript 到鸿蒙的类型升级
07 ArkTS 基础类型与接口定义:从 TypeScript 到鸿蒙的类型升级
前言
图:07 ArkTS 基础类型与接口定义:从 TypeScript 到鸿蒙的类型升级 运行效果截图(HarmonyOS NEXT)
ArkTS是 HarmonyOS NEXT 的官方开发语言,基于 TypeScript 但在类型系统上做了更严格的约束:禁止any类型、禁止动态属性访问、强制接口声明。这些限制让代码更安全、更可预测,但也需要开发者改变一些 TypeScript 习惯。
本文以"鹿鹿·笔迹心理分析"项目中的Models.ets数据模型文件为核心案例,深入解析 ArkTS 的接口定义、联合类型、可选字段、只读数组和as const类型收窄等核心类型特性。
官方文档·ArkTS 基础语法:developer.huawei.com
ArkTS 与 TypeScript 差异:developer.huawei.com
图:ArkTS 类型层次——从基础类型(number/string/boolean)到接口(interface)到数据实体(Entity)
一、ArkTS 与 TypeScript 的关键差异
1.1 类型约束升级
ArkTS 在 TypeScript 基础上增加了以下强制规则:
| 规则 | TypeScript 允许 | ArkTS 要求 |
|---|---|---|
any类型 | ✅ 可以 | ❌ 禁止 |
动态属性访问obj['key'] | ✅ 可以 | ⚠️ 需要先as Record<string, T> |
| 未声明的接口扩展 | ✅ 可以 | ❌ 禁止(需明确接口) |
null和undefined的使用 | 需配置 | 需要可选链?.和空值合并?? |
eval() | ✅ 可以 | ❌ 禁止 |
1.2 ArkTS 中使用 Record 处理动态访问
// ❌ TypeScript 写法(ArkTS 不允许)constvalue=someObj['dynamicKey'];// ✅ ArkTS 写法constrecord=someObjasRecord<string,Object>;constvalue=record['dynamicKey'];// ✅ 实际项目中的应用(路由参数读取)constparams=this.getUIContext().getRouter().getParams()asRecord<string,Object>;if(params&&typeofparams['reportId']==='number'){this.reportId=params['reportId']asnumber;}二、基础类型系统
2.1 原始类型
ArkTS 支持以下原始类型:
// 数字类型(整数和浮点数统一为 number)letage:number=25;letscore:number=3.14;letenergy:number=0;// 字符串类型letname:string='鹿鹿用户';letpath:string='pages/HomePage';// 布尔类型letisLoading:boolean=false;letisBound:boolean=true;// null 和 undefined(需显式声明)letmaybeId:number|null=null;letoptionalText:string|undefined=undefined;2.2 数组类型
// 方式一:T[] 语法(推荐)lettags:string[]=['温柔','细腻','专注'];letscores:number[]=[78,65,92,50,80,70];// 方式二:Array<T> 泛型语法letitems:Array<ArchiveEntity>=[];// 方式三:只读数组(不可修改元素)constLABELS:readonlystring[]=['能量场','思维轴','行动力','情绪值','社交力','细节感'];// ❌ 尝试修改只读数组会报错// LABELS.push('新维度'); // 编译错误// LABELS[0] = '修改'; // 编译错误2.3 联合类型
// 联合类型:值可以是多个类型之一typeInputSource='camera'|'gallery'|'handwrite';typeBoundMethod='tap'|'qrcode'|'phone';// 函数参数使用联合类型functionprocessInput(source:InputSource):void{if(source==='camera'){// 处理相机输入}elseif(source==='gallery'){// 处理相册输入}else{// 处理手写输入}}// 可空联合类型(常见于可选的业务数据)typeArchiveId=number|null;letcurrentArchiveId:ArchiveId=null;三、接口定义(interface)
3.1 项目数据实体接口
"鹿鹿"项目的核心数据模型全部定义在Models.ets中:
// Models.ets — 完整数据实体接口定义// ① 用户实体exportinterfaceUserEntity{id?:number// 可选:新创建时无 id,数据库生成后有open_id?:string// 可选:华为账号 Open ID(本地用户为空)name:string// 必须:用户名称avatar?:string// 可选:头像路径created_at:number// 必须:创建时间戳(Unix timestamp)}// ② 档案实体(一个档案 = 为某人建立的分析文件夹)exportinterfaceArchiveEntity{id?:numberuser_id:number// 归属的用户 IDname:string// 档案名称(如"小明"、"自己")emoji?:string// 头像 emoji(如 "🦁")relation?:string// 与档案主体的关系(如"自己"、"男友")color?:string// 档案主题色last_record_at?:number// 最近一条记录的时间戳record_count:number// 分析记录总数created_at:number}// ③ 手写记录实体(一次分析 = 一条手写记录)exportinterfaceHandwritingEntity{id?:numberarchive_id?:numbersource:string// 输入来源:'camera' | 'gallery' | 'handwrite'image_path?:string// 图片文件路径(本地)ocr_text?:string// OCR 识别出的文字feature_json?:string// 6 维特征向量 JSON 字符串energy_score?:number// 能量得分 0-100device_type?:string// 设备类型word_count?:number// 识别字数write_duration?:number// 书写时长(毫秒)created_at:number}3.2 报告和关系实体
// ④ 分析报告实体exportinterfaceReportEntity{id?:numberhandwriting_id:number// 关联的手写记录 IDarchive_id?:number// 关联的档案 IDpersonality_type?:string// 人格类型标签summary?:string// AI 综合解读文字radar_json?:string// 6 维雷达分数 JSON:{ energy, thinking, action, emotion, social, detail }tags_json?:string// 画像标签 JSON 数组:["温柔守护", "敏感细腻"]created_at:number}// ⑤ 伴侣关系实体exportinterfaceRelationEntity{id?:numberuser_id:number// 当前用户 IDpartner_archive_id:number// 伴侣档案 IDbound_method?:string// 绑定方式:'tap' | 'qrcode' | 'phone'match_score?:number// 综合匹配度 0-100created_at:number}3.3 值对象接口
除了数据库实体接口,还有用于 UI 展示的值对象接口:
// 6 维雷达图分数(从 radar_json 解析而来)exportinterfaceRadarScores{energy:number// 能量场 0-100thinking:number// 思维轴 0-100action:number// 行动力 0-100emotion:number// 情绪值 0-100social:number// 社交力 0-100detail:number// 细节感 0-100}// 雷达图数据点(用于 HandwritingRadar 组件)exportinterfaceRadarPoint{label:string// 维度名称(如"能量场")value:number// 维度分数 0-100}// AI 分析结果(传递给 AnalyzingPage → ReportDetailPage 的中间态)exportinterfaceAnalysisResult{ocrText:stringfeatureScores:RadarScores personalityType:stringsummary:stringtags:string[]energyScore:numberimagePath:stringanalyzedAt:number}四、可选字段与空值处理
4.1 可选字段(? 操作符)
interfaceUserEntity{id?:number// 可选:? 表示该字段可以不存在name:string// 必须:不加 ? 表示创建时必须提供}// 创建时不需要提供 id(数据库自动生成)constnewUser:UserEntity={name:'鹿鹿用户',created_at:Date.now()// id 省略,合法};// 从数据库读取后有 idconstexistingUser:UserEntity={id:1,name:'鹿鹿用户',created_at:1700000000};4.2 可选链操作符(?.)
// 安全访问可能为 null 或 undefined 的属性constarchiveName=archive?.name??'未知档案';constscore=report?.radar_json?JSON.parse(report.radar_json):null;// 多层链式访问constenergyScore=result?.featureScores?.energy??0;// 函数调用(当函数可能不存在时)onCtaClick?.();// 如果 onCtaClick 是 undefined,则不调用4.3 空值合并操作符(??)
// ?? 只在左侧为 null 或 undefined 时取右侧的值// 注意区别于 ||(|| 在左侧为任何假值时都取右侧)constuserId=AppStorage.get<number>('user_id')??1;// 默认用户 ID 1constuserName=AppStorage.get<string>('user_name')??'用户';// ?? vs ||constscore=value??0;// value = 0 时,结果是 0(?? 不处理 0)constscore2=value||0;// value = 0 时,结果是 0(|| 把 0 当假值)// 注意:两者结果相同,但当 value = '' 时行为不同五、高级类型特性
5.1 as const 类型收窄
as const将数组或对象字面量升级为只读的精确字面量类型:
// 不使用 as const:类型为 string[],可以 push/修改constTYPES=['温和理性型','温柔守护型','敏感细腻型'];// 使用 as const:类型为 readonly ['温和理性型', '温柔守护型', '敏感细腻型']exportconstPERSONALITY_TYPES=['温和理性型','温柔守护型','敏感细腻型','紧绷专注型','开朗外放型','内敛深沉型']asconst;// 利用 typeof T[number] 提取值作为联合类型exporttypePersonalityType=typeofPERSONALITY_TYPES[number];// 等价于:'温和理性型' | '温柔守护型' | '敏感细腻型' | '紧绷专注型' | '开朗外放型' | '内敛深沉型'5.2 类型断言(as)
// 将宽泛类型断言为具体类型constparams=this.getUIContext().getRouter().getParams()asRecord<string,Object>;constreportId=params['reportId']asnumber;// 从 JSON 字符串解析时的类型断言constscores=JSON.parse(report.radar_json??'{}')asRadarScores;consttags=JSON.parse(report.tags_json??'[]')asstring[];// 注意:类型断言不做运行时检查,断言错误不会报错但会导致运行时异常5.3 泛型(Generic Types)
// 简单泛型函数functionsafeGet<T>(key:string,defaultValue:T):T{returnAppStorage.get<T>(key)??defaultValue;}// 使用泛型constuserId=safeGet<number>('user_id',1);constuserName=safeGet<string>('user_name','用户');// 泛型接口interfaceApiResponse<T>{code:number;message:string;data:T;}// 具体化typeUserResponse=ApiResponse<UserEntity>;typeArchiveListResponse=ApiResponse<ArchiveEntity[]>;六、JSON 序列化与反序列化
6.1 数据库与实体的映射
项目中有几个字段在数据库中存储为 JSON 字符串,需要序列化/反序列化:
// 写入数据库前:对象 → JSON 字符串constradarJson=JSON.stringify({energy:78,thinking:65,action:92,emotion:50,social:80,detail:70}asRadarScores);// 从数据库读取后:JSON 字符串 → 对象constradar=JSON.parse(report.radar_json??'{}')asRadarScores;consttags=JSON.parse(report.tags_json??'[]')asstring[];// 安全的解析(防止 JSON 格式异常)functionsafeParseJson<T>(jsonStr:string|undefined,fallback:T):T{if(!jsonStr)returnfallback;try{returnJSON.parse(jsonStr)asT;}catch(e){hilog.warn(0x0000,'Models','JSON 解析失败: %{public}s',jsonStr);returnfallback;}}// 使用constscores=safeParseJson<RadarScores>(report.radar_json,{energy:50,thinking:50,action:50,emotion:50,social:50,detail:50});6.2 实体与 ValuesBucket 的映射
// 实体接口 → RDB ValuesBucket(写入数据库)functiontoValuesBucket(entity:HandwritingEntity):relationalStore.ValuesBucket{return{archive_id:entity.archive_id??0,source:entity.source,image_path:entity.image_path??'',ocr_text:entity.ocr_text??'',feature_json:entity.feature_json??'',energy_score:entity.energy_score??0,word_count:entity.word_count??0,created_at:entity.created_at};}七、ArkTS 编码规范总结
7.1 接口设计最佳实践
| 建议 | 理由 | 示例 |
|---|---|---|
数据库实体 id 设为可选(id?) | 新建时无 id | id?: number |
时间戳使用number(Unix 毫秒) | 存储为整数 | created_at: number |
枚举值使用as const+ 联合类型 | 类型安全的枚举 | 'camera' | 'gallery' |
| 复杂对象在数据库中存为 JSON | SQLite 无 JSON 类型 | radar_json: string |
| 可选字段提供空值默认值 | 避免 null 异常 | ?? {}/?? [] |
7.2 常见 ArkTS 类型错误
// ❌ 错误:使用 any 类型constdata:any=response.data;// ✅ 正确:明确类型constdata:AnalysisResult=response.dataasAnalysisResult;// ❌ 错误:直接动态属性访问constname=obj[key];// ✅ 正确:类型断言后访问constrecord=objasRecord<string,string>;constname=record[key];// ❌ 错误:不处理可空值constlength=str.length;// str 可能为 undefined// ✅ 正确:使用可选链constlength=str?.length??0;八、注意事项与常见问题
8.1 开发注意事项
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期:合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
8.2 常见错误与解决方案
开发过程中的高频问题汇总:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 权限被系统拒绝 | 未在module.json5中静态声明 | 添加requestPermissions权限配置项 |
| 页面跳转后白屏 | 路由路径未在main_pages.json注册 | 检查并补充路由配置文件 |
| UI 状态不刷新 | 状态变量未添加响应式装饰器 | 为变量加@State/@Observed装饰器 |
| 数据库查询返回空 | 查询条件与存储数据格式不匹配 | 使用hilog.debug打印 SQL 条件排查 |
| 相机预览黑屏 | 运行时相机权限未获取 | 先调用requestPermissionsFromUser申请 |
总结
ArkTS 的类型系统在 TypeScript 基础上做了严格升级,核心要点:
- 接口设计:使用
interface声明数据实体,区分必须字段和可选字段(?) - 联合类型:使用
'a' | 'b' | 'c'替代string枚举,提供编译时安全 - as const:将常量数组升级为只读字面量类型,配合
typeof T[number]生成联合类型 - 空值处理:使用
?.(可选链)和??(空值合并)安全处理 null/undefined - JSON 字段:数据库中存 JSON 字符串,读取后用
JSON.parse() as T还原类型
下一篇预告:第08篇 ArkTS 类与静态方法设计
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- ArkTS 基础语法指南
- TypeScript 与 ArkTS 差异说明
- 接口(interface)文档
- ArkTS 编码规范
- TypeScript 类型系统参考
- 第06篇 目录结构
- 第08篇 类与静态方法
- 第09篇 异步编程
