零基础鸿蒙应用开发第十八节:内置泛型工具类型应用
【学习目标】
- 掌握
Partial、Required、Record、Readonly工具类型的基础用法与适用场景; - 理解ArkTS禁用
Object.assign/展开运算符的原因,掌握自定义合并函数的实现; - 结合鸿蒙开发场景,用泛型工具类型优化类型约束,避免重复定义相似接口。
【学习重点】
- 核心工具类型:Partial(字段可选化)、Required(字段必填化)、Record(键值对构造)、Readonly(只读约束);
- 核心对比:TS标准合并语法 vs ArkTS自定义合并函数;
- 核心规范:ArkTS强类型约束下的工具类型使用准则。
一、工程结构
本节我们将创建名为GenericToolsDemo的工程,基于 鸿蒙5.0(API12) 开发,使用 DevEco Studio 6.0+ 工具,项目结构目录如下:
GenericToolsDemo/
├── AppScope # 应用全局配置
├── entry # 主模块目录
│ ├── src
│ │ ├── main
│ │ │ ├── ets
│ │ │ │ ├── entryability # 应用入口
│ │ │ │ ├── pages # 页面目录
│ │ │ │ │ └── Index.ets # 测试页面
│ │ │ │ └── utils # 工具函数目录
│ │ │ │ ├── InterfaceTest.ets # 工具类型核心演示
│ │ │ │ ├── ObjectMergeTsTest.ts # TS合并语法(对比用)
│ │ │ │ └── ObjectMergeArkTest.ets # ArkTS合并函数
│ │ └── resources # 静态资源
└── oh_modules # 依赖包
二、基础接口定义
先定义核心基础接口,作为后续工具类型的演示载体:
// utils/InterfaceTest.ets
/**
* 基础用户接口:核心类型模板
*/
export interface User {
id: number; // 必选属性:用户ID
name: string; // 必选属性:用户名
age?: number; // 可选属性:年龄
email?: string; // 可选属性:邮箱
readonly createTime: string; // 只读属性:创建时间(初始化后不可修改)
}
/**
* 应用配置接口:用于Readonly工具类型演示
*/
export interface AppConfig {
baseUrl: string; // 接口基础地址
timeout: number; // 请求超时时间
version: string; // 应用版本
}
三、核心工具类型详解
3.1 Partial:字段可选化
作用
将目标接口的所有必选属性转为可选,适配“局部更新、草稿填充”等无需传递完整字段的场景(如用户资料编辑仅修改部分字段)。
代码示例
// utils/InterfaceTest.ets
/**
* 测试Partial工具类型
*/
export function testPartial(): void {
// 基于User生成全字段可选的类型
type UserPartial = Partial<User>;
// 场景1:仅更新用户邮箱(无需传其他字段)
const updateUser: UserPartial = {
email: "zhangsan@example.com"
};
console.log("\n【Partial】用户局部更新参数:", JSON.stringify(updateUser));
// 场景2:初始化用户草稿(仅填部分核心字段)
const userDraft: UserPartial = {
name: "李四",
createTime: new Date().toISOString()
};
console.log("\n【Partial】用户草稿信息:", JSON.stringify(userDraft));
}
运行效果
【Partial】用户局部更新参数: {"email":"zhangsan@example.com"}
【Partial】用户草稿信息: {"name":"李四","createTime":"2026-01-07T10:20:30.123Z"}
3.2 Required:字段必填化
作用
将目标接口的所有可选属性转为必选,适配“注册提交、核心数据校验”等需要完整字段的场景(如用户注册必须填写所有信息)。
代码示例
// utils/InterfaceTest.ets
/**
* 测试Required工具类型
*/
export function testRequired(): void {
// 基于User生成全字段必填的类型
type UserRequired = Required<User>;
// 场景:用户注册(必须填写所有字段,包括原可选的age/email)
const registerUser: UserRequired = {
id: 1001,
name: "张三",
age: 25,
email: "zhangsan@example.com",
createTime: new Date().toISOString()
};
console.log("\n【Required】用户注册完整信息:", JSON.stringify(registerUser));
// ❌ 错误示例:缺少必填字段(编译报错)
// const errorUser: UserRequired = { id: 1001, name: "张三" };
}
运行效果
【Required】用户注册完整信息: {"id":1001,"name":"张三","age":25,"email":"zhangsan@example.com","createTime":"2026-01-07T10:20:30.123Z"}
3.3 Record:固定键值对约束
作用
约束“固定键集合 + 统一值类型”,适配网络请求参数、接口响应等需要固定键名的场景,替代重复定义接口。
代码示例
// utils/InterfaceTest.ets
/**
* 测试Record工具类型
*/
export function testRecord(): void {
// 场景1:约束网络请求参数(固定键:url/method/timeout)
type ApiRequestParams = Record<"url" | "method" | "timeout", string | number>;
const userRequest: ApiRequestParams = {
"url": "https://api.example.com/user",
"method": "GET",
"timeout": 5000
};
console.log("\n【Record】网络请求参数:", JSON.stringify(userRequest));
// 场景2:约束接口响应数据(固定键:code/msg/data)
type UserApiResponse = Record<"code" | "msg" | "data", number | string | User>;
const userResponse: UserApiResponse = {
"code": 200,
"msg": "请求成功",
"data": { id: 1, name: "张三", createTime: "2026-01-05T08:00:00.000Z" }
};
console.log("\n【Record】网络响应数据:", JSON.stringify(userResponse));
}
运行效果
【Record】网络请求参数: {"url":"https://api.example.com/user","method":"GET","timeout":5000}
【Record】网络响应数据: {"code":200,"msg":"请求成功","data":{"id":1,"name":"张三","createTime":"2026-01-05T08:00:00.000Z"}}
3.4 Readonly:只读约束
作用
批量将目标接口的所有属性标记为只读,适配“应用配置、常量对象”等不允许修改的场景,防止代码意外修改核心数据。
代码示例
// utils/InterfaceTest.ets
/**
* 测试Readonly工具类型
*/
export function testReadonly(): void {
// 1. 基于AppConfig生成只读版本
type ReadonlyAppConfig = Readonly<AppConfig>;
const appConfig: ReadonlyAppConfig = {
baseUrl: "https://api.example.com",
timeout: 5000,
version: "1.0.0"
};
console.log("\n【Readonly】应用配置:", JSON.stringify(appConfig));
console.log("\n【Readonly】读取配置版本:", appConfig.version);
// ❌ 错误示例:修改只读属性(编译报错)
// appConfig.timeout = 8000;
// 2. 直接给Record类型添加只读约束
const readonlyRequest: Readonly<Record<"url" | "method", string>> = {
"url": "https://api.example.com/user",
"method": "GET"
};
console.log("\n【Readonly】只读请求配置:", JSON.stringify(readonlyRequest));
// ❌ 错误示例:修改只读属性(编译报错)
// readonlyRequest.method = "POST";
}
运行效果
【Readonly】应用配置: {"baseUrl":"https://api.example.com","timeout":5000,"version":"1.0.0"}
【Readonly】读取配置版本: 1.0.0
【Readonly】只读请求配置: {"url":"https://api.example.com/user","method":"GET"}
四、TS vs ArkTS 对象合并方案
ArkTS为优化性能,禁用了TS的Object.assign和展开运算符({...obj}),需自定义合并函数替代。
4.1 TS标准合并语法(仅对比)
说明:以下代码在ArkTS中会编译报错,需在ts文件中编译,ArkTS可以调用TS,但TS不可以调用ArkTS。
// utils/ObjectMergeTsTest.ts
import { User } from './InterfaceTest';
/**
* TS标准:Object.assign合并
*/
export function demoTSAssign(): void {
const userInput = { name: "张三", phone: "13800138000" };
const defaultInfo = { avatar: "https://xxx.com/default.png", createTime: new Date().toISOString() };
const mergedUser = Object.assign({}, { id: 1001 }, userInput, defaultInfo);
console.log("\n【TS Object.assign】合并结果:", JSON.stringify(mergedUser));
}
/**
* TS标准:展开运算符合并
*/
export function demoTSSpread(): void {
const baseUser = { id: 1001, name: "张三" };
const extUser = { age: 25, email: "zhangsan@example.com" };
const fullUser = { ...baseUser, ...extUser, createTime: new Date().toISOString() };
console.log("\n【TS 展开运算符】合并结果:", JSON.stringify(fullUser));
}
4.2 ArkTS自定义合并函数(实战可用)
// utils/ObjectMergeArkTest.ets
import { User } from './InterfaceTest';
/**
* ArkTS兼容的对象合并函数(替代Object.assign/展开运算符)
* @param target 目标对象(空对象)
* @param source 待合并的源对象列表
* @returns 合并后的对象
*/
function assign(target: Record<string, Object>, ...source: Object[]): Record<string, Object> {
for (const items of source) {
for (const key of Object.keys(items)) {
target[key] = Reflect.get(items, key);
}
}
return target;
}
/**
* 实战:合并用户信息
*/
export function mergeUserInfo(): void {
// 基础用户信息(符合User接口约束)
const baseUser: User = {
id: 1001,
name: "张三",
createTime: new Date().toISOString()
};
// 扩展用户信息
const extUser: User = {
id: 1001,
name: "张三",
age: 25,
email: "zhangsan@example.com",
createTime: baseUser.createTime
};
// 调用自定义合并函数
const mergedUser = assign({}, baseUser, extUser);
console.log("\n【ArkTS 自定义合并】结果:", JSON.stringify(mergedUser));
}
五、页面调用演示
// pages/Index.ets
import { testPartial, testRequired, testRecord, testReadonly } from '../utils/InterfaceTest';
import { demoTSAssign, demoTSSpread } from '../utils/ObjectMergeTsTest';
import { mergeUserInfo } from '../utils/ObjectMergeArkTest';
@Entry
@Component
struct Index {
build() {
Column({ space: 20 }) {
// 1. 工具类型演示按钮
Button("演示Partial/Required")
.width(220)
.height(60)
.onClick(() => {
testPartial();
testRequired();
});
Button("演示Record")
.width(220)
.height(60)
.onClick(() => testRecord());
Button("演示Readonly")
.width(220)
.height(60)
.onClick(() => testReadonly());
// 2. 合并函数演示按钮
Button("演示TS合并语法(仅对比)")
.width(220)
.height(60)
.onClick(() => {
demoTSAssign();
demoTSSpread();
});
Button("演示ArkTS自定义合并")
.width(220)
.height(60)
.onClick(() => mergeUserInfo());
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20);
}
}
六、完整运行效果
【Partial】用户局部更新参数: {"email":"zhangsan@example.com"}
【Partial】用户草稿信息: {"name":"李四","createTime":"2026-01-07T10:20:30.123Z"}
【Required】用户注册完整信息: {"id":1001,"name":"张三","age":25,"email":"zhangsan@example.com","createTime":"2026-01-07T10:20:30.123Z"}
【Record】网络请求参数: {"url":"https://api.example.com/user","method":"GET","timeout":5000}
【Record】网络响应数据: {"code":200,"msg":"请求成功","data":{"id":1,"name":"张三","createTime":"2026-01-05T08:00:00.000Z"}}
【Readonly】应用配置: {"baseUrl":"https://api.example.com","timeout":5000,"version":"1.0.0"}
【Readonly】读取配置版本: 1.0.0
【Readonly】只读请求配置: {"url":"https://api.example.com/user","method":"GET"}
【TS Object.assign】合并结果: {"id":1001,"name":"张三","phone":"13800138000","avatar":"https://xxx.com/default.png","createTime":"2026-01-07T06:50:17.060Z"}
【TS 展开运算符】合并结果: {"id":1001,"name":"张三","age":25,"email":"zhangsan@example.com","createTime":"2026-01-07T06:50:17.060Z"}
【ArkTS 自定义合并】结果: {"id":1001,"name":"张三","createTime":"2026-01-07T10:20:30.123Z","age":25,"email":"zhangsan@example.com"}
七、核心区别与场景选型表
| 类型/工具 | 核心能力 | 生效阶段 | ArkTS使用规范 | 最佳应用场景 |
|---|---|---|---|---|
interface |
定义对象结构(必选/可选/只读) | 编译期 | 首选,唯一推荐的对象结构约束方式 | 所有需要约束对象结构的基础场景 |
Partial |
全字段可选化 | 编译期 | 仅此时用type派生,其余场景禁用 | 局部更新、草稿数据、部分字段提交 |
Required |
全字段必填化 | 编译期 | 仅此时用type派生,其余场景禁用 | 注册提交、核心数据校验、完整字段提交 |
Record |
约束固定键集合+统一值类型 | 编译期 | 仅此时用type派生,其余场景禁用 | 网络请求参数/响应、固定配置项 |
Readonly |
批量标记属性为只读 | 编译期 | 仅此时用type派生,其余场景禁用 | 应用配置、常量对象、核心数据保护 |
| 普通对象 | 无约束的键值对存储 | 运行时 | 仅用于临时数据,无类型约束场景 | 临时数据、简单数据存储 |
八、开发规范与避坑指南
- type使用限制:仅在使用四大核心工具类型时用
type派生,禁止用type定义字面量对象; - 只读约束仅编译期有效:
Readonly标记的属性运行时可通过解构修改,开发中严禁此类操作; - 非空校验:访问接口属性前必须做非空校验(如
user?.name),避免空指针错误; - Record本质是普通对象:不支持
Map的size/has()方法,仅用于类型约束; - 合并函数规范:必须用
Record<string, unknown>约束类型,避免任意类型传入; - 接口属性完整性:声明接口对象时,必选/只读属性必须初始化,否则编译报错。
九、代码仓库
- 工程名称:GenericToolsDemo
- 仓库地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
十、下节预告
下一节将聚焦 解锁灵活数据存储新技能-集合Map、Set,掌握ArkTS中Map(键值对集合)、Set(无重复值集合)的用法,对比与普通对象/数组的差异,结合购物车、数据去重等场景演示实战用法。
浙公网安备 33010602011771号