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 不允许)
const value = someObj['dynamicKey'];
// ✅ ArkTS 写法
const record = someObj as Record<string, Object>;
const value = record['dynamicKey'];
// ✅ 实际项目中的应用(路由参数读取)
const params = this.getUIContext().getRouter().getParams() as Record<string, Object>;
if (params && typeof params['reportId'] === 'number') {
this.reportId = params['reportId'] as number;
}
二、基础类型系统
2.1 原始类型
ArkTS 支持以下原始类型:
// 数字类型(整数和浮点数统一为 number)
let age: number = 25;
let score: number = 3.14;
let energy: number = 0;
// 字符串类型
let name: string = '鹿鹿用户';
let path: string = 'pages/HomePage';
// 布尔类型
let isLoading: boolean = false;
let isBound: boolean = true;
// null 和 undefined(需显式声明)
let maybeId: number | null = null;
let optionalText: string | undefined = undefined;
2.2 数组类型
// 方式一:T[] 语法(推荐)
let tags: string[] = ['温柔', '细腻', '专注'];
let scores: number[] = [78, 65, 92, 50, 80, 70];
// 方式二:Array<T> 泛型语法
let items: Array<ArchiveEntity> = [];
// 方式三:只读数组(不可修改元素)
const LABELS: readonly string[] = ['能量场', '思维轴', '行动力', '情绪值', '社交力', '细节感'];
// ❌ 尝试修改只读数组会报错
// LABELS.push('新维度'); // 编译错误
// LABELS[0] = '修改'; // 编译错误
2.3 联合类型
// 联合类型:值可以是多个类型之一
type InputSource = 'camera' | 'gallery' | 'handwrite';
type BoundMethod = 'tap' | 'qrcode' | 'phone';
// 函数参数使用联合类型
function processInput(source: InputSource): void {
if (source === 'camera') {
// 处理相机输入
} else if (source === 'gallery') {
// 处理相册输入
} else {
// 处理手写输入
}
}
// 可空联合类型(常见于可选的业务数据)
type ArchiveId = number | null;
let currentArchiveId: ArchiveId = null;
三、接口定义(interface)
3.1 项目数据实体接口
"鹿鹿"项目的核心数据模型全部定义在 Models.ets 中:
// Models.ets — 完整数据实体接口定义
// ① 用户实体
export interface UserEntity {
id?: number // 可选:新创建时无 id,数据库生成后有
open_id?: string // 可选:华为账号 Open ID(本地用户为空)
name: string // 必须:用户名称
avatar?: string // 可选:头像路径
created_at: number // 必须:创建时间戳(Unix timestamp)
}
// ② 档案实体(一个档案 = 为某人建立的分析文件夹)
export interface ArchiveEntity {
id?: number
user_id: number // 归属的用户 ID
name: string // 档案名称(如"小明"、"自己")
emoji?: string // 头像 emoji(如 "🦁")
relation?: string // 与档案主体的关系(如"自己"、"男友")
color?: string // 档案主题色
last_record_at?: number // 最近一条记录的时间戳
record_count: number // 分析记录总数
created_at: number
}
// ③ 手写记录实体(一次分析 = 一条手写记录)
export interface HandwritingEntity {
id?: number
archive_id?: number
source: string // 输入来源:'camera' | 'gallery' | 'handwrite'
image_path?: string // 图片文件路径(本地)
ocr_text?: string // OCR 识别出的文字
feature_json?: string // 6 维特征向量 JSON 字符串
energy_score?: number // 能量得分 0-100
device_type?: string // 设备类型
word_count?: number // 识别字数
write_duration?: number // 书写时长(毫秒)
created_at: number
}
3.2 报告和关系实体
// ④ 分析报告实体
export interface ReportEntity {
id?: number
handwriting_id: number // 关联的手写记录 ID
archive_id?: number // 关联的档案 ID
personality_type?: string // 人格类型标签
summary?: string // AI 综合解读文字
radar_json?: string // 6 维雷达分数 JSON:{ energy, thinking, action, emotion, social, detail }
tags_json?: string // 画像标签 JSON 数组:["温柔守护", "敏感细腻"]
created_at: number
}
// ⑤ 伴侣关系实体
export interface RelationEntity {
id?: number
user_id: number // 当前用户 ID
partner_archive_id: number // 伴侣档案 ID
bound_method?: string // 绑定方式:'tap' | 'qrcode' | 'phone'
match_score?: number // 综合匹配度 0-100
created_at: number
}
3.3 值对象接口
除了数据库实体接口,还有用于 UI 展示的值对象接口:
// 6 维雷达图分数(从 radar_json 解析而来)
export interface RadarScores {
energy: number // 能量场 0-100
thinking: number // 思维轴 0-100
action: number // 行动力 0-100
emotion: number // 情绪值 0-100
social: number // 社交力 0-100
detail: number // 细节感 0-100
}
// 雷达图数据点(用于 HandwritingRadar 组件)
export interface RadarPoint {
label: string // 维度名称(如"能量场")
value: number // 维度分数 0-100
}
// AI 分析结果(传递给 AnalyzingPage → ReportDetailPage 的中间态)
export interface AnalysisResult {
ocrText: string
featureScores: RadarScores
personalityType: string
summary: string
tags: string[]
energyScore: number
imagePath: string
analyzedAt: number
}
四、可选字段与空值处理
4.1 可选字段(? 操作符)
interface UserEntity {
id?: number // 可选:? 表示该字段可以不存在
name: string // 必须:不加 ? 表示创建时必须提供
}
// 创建时不需要提供 id(数据库自动生成)
const newUser: UserEntity = {
name: '鹿鹿用户',
created_at: Date.now()
// id 省略,合法
};
// 从数据库读取后有 id
const existingUser: UserEntity = {
id: 1,
name: '鹿鹿用户',
created_at: 1700000000
};
4.2 可选链操作符(?.)
// 安全访问可能为 null 或 undefined 的属性
const archiveName = archive?.name ?? '未知档案';
const score = report?.radar_json ? JSON.parse(report.radar_json) : null;
// 多层链式访问
const energyScore = result?.featureScores?.energy ?? 0;
// 函数调用(当函数可能不存在时)
onCtaClick?.(); // 如果 onCtaClick 是 undefined,则不调用
4.3 空值合并操作符(??)
// ?? 只在左侧为 null 或 undefined 时取右侧的值
// 注意区别于 ||(|| 在左侧为任何假值时都取右侧)
const userId = AppStorage.get<number>('user_id') ?? 1; // 默认用户 ID 1
const userName = AppStorage.get<string>('user_name') ?? '用户';
// ?? vs ||
const score = value ?? 0; // value = 0 时,结果是 0(?? 不处理 0)
const score2 = value || 0; // value = 0 时,结果是 0(|| 把 0 当假值)
// 注意:两者结果相同,但当 value = '' 时行为不同
五、高级类型特性
5.1 as const 类型收窄
as const 将数组或对象字面量升级为只读的精确字面量类型:
// 不使用 as const:类型为 string[],可以 push/修改
const TYPES = ['温和理性型', '温柔守护型', '敏感细腻型'];
// 使用 as const:类型为 readonly ['温和理性型', '温柔守护型', '敏感细腻型']
export const PERSONALITY_TYPES = [
'温和理性型',
'温柔守护型',
'敏感细腻型',
'紧绷专注型',
'开朗外放型',
'内敛深沉型'
] as const;
// 利用 typeof T[number] 提取值作为联合类型
export type PersonalityType = typeof PERSONALITY_TYPES[number];
// 等价于:'温和理性型' | '温柔守护型' | '敏感细腻型' | '紧绷专注型' | '开朗外放型' | '内敛深沉型'
5.2 类型断言(as)
// 将宽泛类型断言为具体类型
const params = this.getUIContext().getRouter().getParams() as Record<string, Object>;
const reportId = params['reportId'] as number;
// 从 JSON 字符串解析时的类型断言
const scores = JSON.parse(report.radar_json ?? '{}') as RadarScores;
const tags = JSON.parse(report.tags_json ?? '[]') as string[];
// 注意:类型断言不做运行时检查,断言错误不会报错但会导致运行时异常
5.3 泛型(Generic Types)
// 简单泛型函数
function safeGet<T>(key: string, defaultValue: T): T {
return AppStorage.get<T>(key) ?? defaultValue;
}
// 使用泛型
const userId = safeGet<number>('user_id', 1);
const userName = safeGet<string>('user_name', '用户');
// 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 具体化
type UserResponse = ApiResponse<UserEntity>;
type ArchiveListResponse = ApiResponse<ArchiveEntity[]>;
六、JSON 序列化与反序列化
6.1 数据库与实体的映射
项目中有几个字段在数据库中存储为 JSON 字符串,需要序列化/反序列化:
// 写入数据库前:对象 → JSON 字符串
const radarJson = JSON.stringify({
energy: 78,
thinking: 65,
action: 92,
emotion: 50,
social: 80,
detail: 70
} as RadarScores);
// 从数据库读取后:JSON 字符串 → 对象
const radar = JSON.parse(report.radar_json ?? '{}') as RadarScores;
const tags = JSON.parse(report.tags_json ?? '[]') as string[];
// 安全的解析(防止 JSON 格式异常)
function safeParseJson<T>(jsonStr: string | undefined, fallback: T): T {
if (!jsonStr) return fallback;
try {
return JSON.parse(jsonStr) as T;
} catch (e) {
hilog.warn(0x0000, 'Models', 'JSON 解析失败: %{public}s', jsonStr);
return fallback;
}
}
// 使用
const scores = safeParseJson<RadarScores>(
report.radar_json,
{ energy: 50, thinking: 50, action: 50, emotion: 50, social: 50, detail: 50 }
);
6.2 实体与 ValuesBucket 的映射
// 实体接口 → RDB ValuesBucket(写入数据库)
function toValuesBucket(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 类型
const data: any = response.data;
// ✅ 正确:明确类型
const data: AnalysisResult = response.data as AnalysisResult;
// ❌ 错误:直接动态属性访问
const name = obj[key];
// ✅ 正确:类型断言后访问
const record = obj as Record<string, string>;
const name = record[key];
// ❌ 错误:不处理可空值
const length = str.length; // str 可能为 undefined
// ✅ 正确:使用可选链
const length = 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 类与静态方法设计
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)