文章配图:ArkTS 严格模式下的类型收窄技巧

页面预览

前言

ArkTS 是 HarmonyOS 原生应用开发语言,基于 TypeScript 但要求更严格的类型安全——禁止 any 类型禁止动态属性访问。这些约束在编译期发现潜在的类型错误,提升代码质量和运行稳定性。但对于习惯了 TS any 大法的开发者来说,ArkTS 严格模式带来的类型收窄挑战是入门时最常遇到的障碍。

本文以「猫猫大作战」的引擎代码为锚点,讲解 ArkTS 严格模式下的类型收窄技巧,包括联合类型收窄类型守卫可空类型处理类型断言以及常见的编译错误排查

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–116 篇。本篇是阶段四第 117 篇。

一、ArkTS 严格模式核心规则

1.1 禁止 any 类型

// 🚫 错误:ArkTS 中不能使用 any
let data: any = getData();           // 编译错误
let items: Array<any> = [];          // 编译错误

// ✅ 正确:使用明确的类型
let data: Cat | null = getCatData();
let items: Cat[] = [];

1.2 禁止动态属性访问

// 🚫 错误:动态属性访问
const key = 'level';
const value = cat[key];              // 编译错误

// ✅ 正确:直接访问已知属性
const value = cat.level;

// ✅ 正确:使用索引类型
interface CatMap {
  [key: number]: Cat;
}
const cat = catMap[3];
规则 TypeScript ArkTS 严格模式
any 类型 ✅ 允许 ❌ 禁止
动态属性 ✅ 允许 ❌ 禁止
undefined 类型 ✅ 允许 ❌ 使用 X | null
隐式 any 可配置 ❌ 完全禁止
类型断言 as / <> as 语法

提示:ArkTS 严格模式的目标是在编译期消除所有类型不确定性,从而提升运行时性能和稳定性。这些限制在 API 10+ 的项目中默认启用。

二、联合类型与类型收窄

2.1 什么是类型收窄

类型收窄(Type Narrowing)是指通过条件判断,将一个联合类型的变量范围缩小到更具体的子类型:

function processCat(cat: Cat | null) {
  if (cat === null) return;             // 收窄开始
  // 此作用域内 cat 类型已收窄为 Cat
  console.info(cat.level.toString());   // ✅ 安全访问
  console.info(cat.x);                  // ✅ 安全访问
}

2.2 常见的收窄模式

// 模式 1:null 判断收窄
function getLevel(cat: Cat | null): number {
  if (!cat) return 0;
  return cat.level;                     // cat 已收窄为 Cat
}

// 模式 2:typeof 收窄
function mergeCount(val: string | number): number {
  if (typeof val === 'string') {
    return parseInt(val, 10);            // val 收窄为 string
  }
  return val;                            // val 收窄为 number
}

// 模式 3:in 操作符收窄
interface CatItem { level: number; }
interface DogItem { breed: string; }
function identify(obj: CatItem | DogItem) {
  if ('level' in obj) {
    console.info(obj.level);            // obj 收窄为 CatItem
  }
}

// 模式 4:判等收窄
type Direction = 'up' | 'down' | 'left' | 'right';
function move(dir: Direction) {
  if (dir === 'up') {
    // dir 收窄为 'up'
  }
}

三、可空类型处理

3.1 Cat 对象的 null 安全

在「猫猫大作战」的棋盘操作中,很多方法返回 Cat | null

// 来源:GameEngine.ets
getCatAt(x: number, y: number): Cat | null {
  if (x < 0 || x >= GameConfig.BOARD_WIDTH) return null;
  if (y < 0 || y >= GameConfig.BOARD_HEIGHT) return null;
  return this.board[y][x] || null;
}

3.2 使用时的收窄

// 方式 1:if 守卫(推荐)
const cat = gameEngine.getCatAt(2, 3);
if (cat !== null) {
  // cat 已收窄为 Cat
  this.selectedCat = cat;
  this.showDetail = true;
}

// 方式 2:三元表达式
const level = cat !== null ? cat.level : 0;

// 方式 3:空值合并运算符 ?? (API 11+)
const name = cat?.name ?? '未知猫咪';

// 🚫 错误:直接访问可能为 null 的属性
const level = cat.level;  // 编译错误:cat 可能是 null

3.3 可选链操作符 ?.

// 安全访问嵌套属性
const catName = gameEngine.getCatAt(2, 3)?.name ?? '未知';

// 方法链式调用
const merged = gameEngine.tryMergeAt(x, y)?.result ?? MergeResult.NONE;

// 数组安全访问
const firstCat = this.cats?.[0] ?? null;
操作符 作用 示例
?. 可选链 cat?.level
?? 空值合并 level ?? 0
! 非空断言 cat!.level(谨慎使用)

提示:! 非空断言会绕过编译检查,如果运行时值为 null 会崩溃。只在确保 100% 不为 null 时使用,否则优先用 if 收窄。

四、类型守卫与自定义守卫

4.1 typeof 类型守卫

function isNumber(val: string | number): val is number {
  return typeof val === 'number';
}

// 使用
function formatScore(score: string | number): string {
  if (isNumber(score)) {
    return score.toFixed(0);           // score 收窄为 number
  }
  return score;                         // score 收窄为 string
}

4.2 自定义类型守卫

// 判断是否为有效的 Cat 对象
function isValidCat(obj: unknown): obj is Cat {
  return obj !== null
    && typeof obj === 'object'
    && 'id' in obj
    && 'level' in obj
    && 'x' in obj
    && 'y' in obj;
}

// 在游戏引擎中使用
function processCatData(data: unknown): Cat | null {
  if (isValidCat(data)) {
    return data as Cat;                 // 通过守卫后安全断言
  }
  console.warn('无效的猫咪数据', data);
  return null;
}

4.3 判别式联合类型

// 使用 kind 字段做判别式
type GameEvent =
  | { kind: 'MERGE'; cats: Cat[]; score: number }
  | { kind: 'DROP'; col: number; level: number }
  | { kind: 'LEVEL_UP'; newLevel: number }
  | { kind: 'GAME_OVER'; finalScore: number };

function handleEvent(event: GameEvent) {
  switch (event.kind) {
    case 'MERGE':
      console.info(`合并 ${event.cats.length} 猫,得分 ${event.score}`);
      break;
    case 'DROP':
      console.info(`在列 ${event.col} 投放 ${event.level} 级猫`);
      break;
    case 'LEVEL_UP':
      console.info(`升级到 ${event.newLevel}`);
      break;
    case 'GAME_OVER':
      console.info(`游戏结束:${event.finalScore}`);
      break;
  }
}

五、类型断言的使用

5.1 as 语法

// 当开发者确定类型更具体时,使用 as 断言
const element = document.getElementById('game-canvas') as HTMLCanvasElement;

// 在游戏中的使用
const rawData = this.gameEngine.getRawData();
const board = rawData as Cat[][];         // 断言为二维数组

5.2 断言的注意事项

// ⚠️ 谨慎:断言会绕过编译检查
const cat = null as unknown as Cat;       // 运行时仍是 null
cat.level = 5;                            // 运行时崩溃!

// ✅ 正确:先守卫再断言
function safeCast(data: unknown): Cat {
  if (isValidCat(data)) {
    return data as Cat;
  }
  throw new Error('无效的猫咪数据');
}

六、在游戏引擎中的实际应用

6.1 mergeCats 的类型安全

// 来源:GameEngine.ets
mergeCats(cats: (Cat | null)[]): Cat | null {
  // 过滤掉 null
  const validCats = cats.filter(c => c !== null) as Cat[];
  if (validCats.length < 2) return null;

  // 检查等级是否一致
  const firstLevel = validCats[0].level;
  const allSameLevel = validCats.every(c => c.level === firstLevel);
  if (!allSameLevel) return null;

  // 创建下一级猫咪
  const newLevel = firstLevel + 1;
  const midX = validCats.reduce((sum, c) => sum + c.x, 0) / validCats.length;
  const midY = validCats.reduce((sum, c) => sum + c.y, 0) / validCats.length;

  return new Cat(`cat_${Date.now()}`, newLevel, Math.round(midX), Math.round(midY), false);
}

6.2 棋盘查找的类型收窄

findMergeableCats(): { x: number; y: number; cat: Cat }[] {
  const result: { x: number; y: number; cat: Cat }[] = [];

  for (let y = 0; y < GameConfig.BOARD_HEIGHT; y++) {
    for (let x = 0; x < GameConfig.BOARD_WIDTH; x++) {
      const cat = this.board[y][x];
      if (cat !== null) {
        // 收窄:cat 从 Cat | null 变为 Cat
        result.push({ x, y, cat });
      }
    }
  }
  return result;
}

七、常见编译错误与排查

7.1 “Type ‘X | null’ is not assignable to type ‘X’”

// 🚫 错误
let cat: Cat = gameEngine.getCatAt(0, 0);  // getCatAt 返回 Cat | null

// ✅ 修复 1:if 收窄
const maybeCat = gameEngine.getCatAt(0, 0);
if (maybeCat) {
  let cat: Cat = maybeCat;
}

// ✅ 修复 2:空值合并
let cat: Cat = gameEngine.getCatAt(0, 0) ?? defaultCat;

7.2 “Property ‘xxx’ does not exist on type ‘never’”

// 🚫 错误:收窄后所有分支都覆盖了但仍报 never
function process(val: string | number) {
  if (typeof val === 'string') { /* ... */ }
  else if (typeof val === 'number') { /* ... */ }
  else {
    const impossible: never = val;  // val 是 never,所有分支已覆盖
  }
}

7.3 类型收窄检查清单

问题 可能原因 解决方法
null 不可赋值 未做 null 收窄 if (x !== null)
动态属性报错 使用了 obj[key] 改用直接属性访问
as 断言报错 断言到不兼容类型 先用类型守卫验证
filter 后类型未收窄 TS 不能推断 filter 效果 手动 as Cat[] 断言

八、进阶:泛型约束与类型收窄

8.1 泛型约束

// 泛型约束确保类型安全
function getFirstCat<T extends Cat>(cats: T[]): T | null {
  return cats.length > 0 ? cats[0] : null;
}

// 使用
const cats: Cat[] = [new Cat('1', 1, 0, 0, false)];
const first = getFirstCat(cats);  // first 类型为 Cat | null

8.2 映射类型

// 只读版本
type ReadonlyCat = {
  readonly [K in keyof Cat]: Cat[K];
};

// 可选版本
type PartialCat = {
  [K in keyof Cat]?: Cat[K];
};

// 使用
function freezeCat(cat: Cat): ReadonlyCat {
  return Object.freeze({ ...cat });
}

九、严格模式的最佳实践

  1. 永远不要用 any:改用 unknown + 类型守卫
  2. null 收窄优先用 ifif (x !== null)!x 更精确
  3. 可选链 + 空值合并cat?.name ?? '默认' 一行处理可空
  4. 自定义类型守卫:复杂类型检查封装为 isXxx(val) 函数
  5. 判别式联合类型:用 kind 字段做 switch 收窄
  6. as 断言只在确定时用:不确定时先用守卫验证
// ✅ 推荐的完整模式
function safeGetCatLevel(engine: GameEngine, x: number, y: number): number {
  const cat = engine.getCatAt(x, y);     // Cat | null
  if (cat === null) return 0;            // 收窄
  return cat.level;                       // 安全访问
}

十、对比其他语言的类型系统

特性 ArkTS 严格模式 TypeScript Rust Kotlin
空安全 `Type null` `Type null`
类型收窄 if/switch/as if/switch/as/in match when
类型守卫 自定义函数 自定义函数 if let 智能转换
运行时开销 零(编译期) 零(编译期) 零(编译期) 零(编译期)

总结

ArkTS 严格模式通过禁止 any、禁止动态属性、强制空安全等规则,在编译期消灭类型不确定性。类型收窄是开发者最常用的技巧——用 if (x !== null) 收窄可空类型、用 typeof 收窄基本类型、用 switch(kind) 收窄联合类型、用自定义守卫验证复杂类型。核心要点:收窄靠条件、 类型守卫封装复用、 as 断言审慎使用、 可选链+空值合并兜底

下一篇将深入 constructor——GameEngine 构造器初始化与依赖注入。

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


相关资源:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐