零基础鸿蒙应用开发第十八节:内置泛型工具类型应用

零基础鸿蒙应用开发学习计划表

【学习目标】

  1. 掌握PartialRequiredRecordReadonly工具类型的基础用法与适用场景;
  2. 理解ArkTS禁用Object.assign/展开运算符的原因,掌握自定义合并函数的实现;
  3. 结合鸿蒙开发场景,用泛型工具类型优化类型约束,避免重复定义相似接口。

【学习重点】

  1. 核心工具类型:Partial(字段可选化)、Required(字段必填化)、Record(键值对构造)、Readonly(只读约束);
  2. 核心对比:TS标准合并语法 vs ArkTS自定义合并函数;
  3. 核心规范: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派生,其余场景禁用 应用配置、常量对象、核心数据保护
普通对象 无约束的键值对存储 运行时 仅用于临时数据,无类型约束场景 临时数据、简单数据存储

八、开发规范与避坑指南

  1. type使用限制:仅在使用四大核心工具类型时用type派生,禁止用type定义字面量对象;
  2. 只读约束仅编译期有效Readonly标记的属性运行时可通过解构修改,开发中严禁此类操作;
  3. 非空校验:访问接口属性前必须做非空校验(如user?.name),避免空指针错误;
  4. Record本质是普通对象:不支持Mapsize/has()方法,仅用于类型约束;
  5. 合并函数规范:必须用Record<string, unknown>约束类型,避免任意类型传入;
  6. 接口属性完整性:声明接口对象时,必选/只读属性必须初始化,否则编译报错。

九、代码仓库

十、下节预告

下一节将聚焦 解锁灵活数据存储新技能-集合Map、Set,掌握ArkTS中Map(键值对集合)、Set(无重复值集合)的用法,对比与普通对象/数组的差异,结合购物车、数据去重等场景演示实战用法。

posted @ 2026-01-21 16:22  鸿蒙-散修  阅读(0)  评论(0)    收藏  举报