前言
ArkTS 是 HarmonyOS 原生应用开发语言,基于 TypeScript 但要求更严格的类型安全——禁止any类型、禁止动态属性访问。这些约束在编译期发现潜在的类型错误,提升代码质量和运行稳定性。但对于习惯了 TSany大法的开发者来说,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 可能是 null3.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 | null8.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 构造器初始化与依赖注入。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- ArkTS 严格模式官方文档
- ArkTS 类型系统指南
- TypeScript 类型收窄
- ArkTS 空安全
- ArkTS 与 TypeScript 差异对比
- 联合类型官方指南
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档
- 第 116 篇:interface 接口定义
- 第 118 篇:constructor 构造器