HarmonyOS应用开发实战:猫猫大作战-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 });
}
九、严格模式的最佳实践
- 永远不要用
any:改用unknown+ 类型守卫 - null 收窄优先用
if:if (x !== null)比!x更精确 - 可选链 + 空值合并:
cat?.name ?? '默认'一行处理可空 - 自定义类型守卫:复杂类型检查封装为
isXxx(val)函数 - 判别式联合类型:用
kind字段做 switch 收窄 - 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 构造器初始化与依赖注入。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐


所有评论(0)