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 类型系统层次图

图:ArkTS 类型层次——从基础类型(number/string/boolean)到接口(interface)到数据实体(Entity)


一、ArkTS 与 TypeScript 的关键差异

1.1 类型约束升级

ArkTS 在 TypeScript 基础上增加了以下强制规则:

规则 TypeScript 允许 ArkTS 要求
any 类型 ✅ 可以 ❌ 禁止
动态属性访问 obj['key'] ✅ 可以 ⚠️ 需要先 as Record<string, T>
未声明的接口扩展 ✅ 可以 ❌ 禁止(需明确接口)
nullundefined 的使用 需配置 需要可选链 ?. 和空值合并 ??
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 基础上做了严格升级,核心要点:

  1. 接口设计:使用 interface 声明数据实体,区分必须字段和可选字段(?
  2. 联合类型:使用 'a' | 'b' | 'c' 替代 string 枚举,提供编译时安全
  3. as const:将常量数组升级为只读字面量类型,配合 typeof T[number] 生成联合类型
  4. 空值处理:使用 ?.(可选链)和 ??(空值合并)安全处理 null/undefined
  5. JSON 字段:数据库中存 JSON 字符串,读取后用 JSON.parse() as T 还原类型

下一篇预告第08篇 ArkTS 类与静态方法设计

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐