【HarmonyOS开发小实践】ArkTS相比于TypeScript扩展了什么,限制了什么
写过 TS 的同学转 ArkTS,多半会经历一个过程:开头觉得"这不就是 TS 吗",写着写着发现有些代码编译不过,再细看才知道 ArkTS 在 TS 基础上既加了东西也砍了东西。这篇就把 ArkTS 相对 TS 的"加法"和"减法"列清楚,再说说为什么要做这些取舍,以及从 TS 迁移代码时要注意什么。
先把关系摆正
ArkTS 在 TypeScript 生态基础上做了进一步扩展,保持了 TS 的基本风格。关键词是"扩展"和"保持"——不是另起炉灶,是在 TS 上做加减。
ArkTS 保留了 TS 大部分语法特性。没在约束文档里提到的 TS 特性,ArkTS 完全支持。比如自定义装饰器,语法跟 TS 一致。所以写 ArkTS 不会觉得是完全陌生的语言,TS 的经验大部分能复用。
但 ArkTS 的取向跟 TS 不一样。TS 的设计目标是"给 JS 加类型,渐进式采用",类型是可选的、可擦除的;ArkTS 的设计目标是"给 HarmonyOS 应用开发提供高性能、高稳定性的语言",类型是强制的、参与编译优化的。取向不同,做出来的加减法就不同。
扩展了什么
ArkTS 相对 TS 的扩展,主要在三块:状态管理装饰器、UI 描述能力、并发能力增强。
状态管理装饰器
这是 ArkTS 最显眼的扩展。TS 里装饰器是个实验性特性,用法比较克制;ArkTS 把装饰器做成了语言的核心能力,专门用来描述 UI 组件的状态和关系。
@Entry // 标记入口组件
@Component // 标记自定义组件
struct Counter {
@State count: number = 0; // 状态变量,变化触发 UI 刷新
@Prop maxCount: number = 10; // 单向同步,父到子
@Link sharedValue: number; // 双向同步
@Watch('onCountChange')
@State watched: number = 0; // @Watch 监听变化
onCountChange() {
console.info(`count 变了: ${this.count}`);
}
build() {
Column() {
Text(`${this.count} / ${this.maxCount}`)
}
}
}
这套装饰器体系是 ArkUI 框架的配套,但语法层面是 ArkTS 提供的。@State、@Prop、@Link、@Watch、@Provide、@Consume、@Observed、@ObjectLink 这些,构成了 ArkTS 的状态管理能力。
UI 描述能力
ArkTS 配合 ArkUI 提供了一套声明式的 UI 描述语法。build() 方法里的 Column()、Row()、Text()、Button() 这些不是普通函数调用,是 ArkUI 的组件构造 DSL。这套 DSL 嵌在 ArkTS 语法里,由 ArkTS 编译器特殊处理。
@Component
struct Greeting {
@Prop name: string;
build() {
Column() {
Text(`Hello, ${this.name}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button('点我')
.onClick(() => {
console.info('clicked');
})
}
.padding(20)
}
}
TS 本身没有 UI 描述能力,要写 UI 得配 React、Vue 这样的框架。ArkTS 把 UI 描述做进了语言层面,跟状态管理装饰器配合,形成一套自洽的开发范式。
并发能力增强
TS/JS 的并发主要靠 Promise、async/await 和 Worker。ArkTS 在这基础上做了增强:
- 提供 TaskPool:轻量级任务池,自动管理线程,适合短任务
- 提供 Worker:重量级线程,手动管理,适合长任务
- 引入 Sendable 概念:支持对象在并发实例间引用传递,提升通信性能
- 提供 @Concurrent 装饰器:标记可以在子线程执行的函数
import { taskpool } from '@kit.ArkTS';
@Concurrent
function processData(data: number[]): number {
return data.reduce((sum, n) => sum + n, 0);
}
let task = new taskpool.Task(processData, [1, 2, 3, 4, 5]);
taskpool.execute(task).then((result: Object) => {
console.info(`结果: ${result}`);
});
基础类库增强
ArkTS 的基础类库比标准 TS 更丰富,提供了高精度数学库 Decimal、Buffer 与 FastBuffer、XML 处理、多种容器库等。这些是应用开发常用但 TS 标准库没有的。
限制了什么
扩展的部分好理解,限制的部分才是从 TS 迁过来踩坑的地方。ArkTS 的限制主要围绕两个目标:降低运行时性能开销 和 减少动态特性带来的不确定性。
禁止 any 和 unknown
这是最直接的限制。any 和 unknown 在 ArkTS 里不能用,必须显式指定具体类型。
// TS 可以这么写
// let value: any = 42;
// let data: unknown = 'hello';
// ArkTS 必须写具体类型
let value: number = 42;
let data: string = 'hello';
// 或者用 Object
let obj: Object = 42;
官方的说法是 any 在 TS 代码库里使用率约 1%,禁掉影响不大,但能换来明显的性能提升——因为编译器不用在运行时做类型检查了。
禁止运行时变更对象布局
TS 里可以动态给对象加属性、删属性(虽然严格模式下会报错,但能用 as any 绕过)。ArkTS 彻底堵死了这条路:
class Point {
public x: number = 0;
public y: number = 0;
}
let p = new Point();
// TS 里能这么写(虽然不规范)
// (p as any).z = 'label';
// ArkTS 里编译报错
// p.z = 'label'; // 报错
// delete (p as any).x; // 报错,as any 也绕不过
对象布局固定,意味着运行时不用为"对象可能多出属性"预留空间,访问属性可以直接按偏移量取,性能更好。
不支持 structural typing
这个限制比较隐蔽。TS 支持 structural typing——两个类型只要结构相同,就算没有继承关系也能互相赋值。ArkTS 不支持,必须通过继承或 implements 接口建立显式关系。
// TS 里这段能编译
// class T { name: string = ''; }
// class U { name: string = ''; }
// let u: U = new T(); // 结构相同,TS 允许
// ArkTS 里报错,必须显式建立关系
interface INamed {
name: string;
}
class T implements INamed {
name: string = '';
}
class U implements INamed {
name: string = '';
}
let u: INamed = new T(); // 通过接口建立关系,合法
官方的理由是:支持 structural typing 需要在语言规范、编译器和运行时做大量工作,而且运行时为了支持这个特性需要额外的性能开销。当前不支持,后续根据反馈再考虑。
不支持解构赋值
TS 里的解构赋值很方便,ArkTS 不支持:
// TS
// let [one, two] = [1, 2];
// let { x, y } = somePoint;
// ArkTS 要这么写
let arr: number[] = [1, 2];
let one = arr[0];
let two = arr[1];
let point = somePoint;
let x = point.x;
let y = point.y;
解构赋值依赖结构兼容性,是个偏动态的特性,跟 ArkTS 的静态取向冲突。
其他主要限制
把其他常见限制列个表,方便对照:
| 限制项 | 说明 | 替代方案 |
|---|---|---|
禁止 var | 必须用 let 或 const | 用 let |
| 禁止函数表达式 | 不能写 function() {} | 用箭头函数 () => {} |
| 禁止函数内声明函数 | 不能在函数里 function foo() | 用 lambda |
禁止 Function.apply/call/bind | this 语义限制 | 用 OOP 风格 |
禁止 # 私有字段 | 不支持 #foo 语法 | 用 private 关键字 |
禁止 @ts-ignore | 不能跳过类型检查 | 修正类型 |
| 禁止生成器函数 | 不支持 function* | 用 async/await |
| 禁止 index signature | 不支持 [key: string]: T | 用 Map 或 Record |
| 禁止 intersection type | 不支持 A & B | 用接口继承 |
| 禁止条件类型 | 不支持 T extends U ? X : Y | 用显式泛型约束 |
| 禁止映射类型 | 不支持 { [K in keyof T]: ... } | 显式定义类型 |
| 禁止类表达式 | 不能 const C = class {} | 显式声明 class |
| 禁止原型赋值 | 不支持 C.prototype.foo = ... | 用 class 定义 |
| 对象字面量需标类型 | 不能写 let o = { x: 1 } 不标类型 | 显式标注类型 |
| 禁止确定赋值断言 | 不支持 let x!: number | 声明时初始化 |
强制严格类型检查
TS 的严格模式是可配置的,strictNullChecks、noImplicitReturns 这些可以开关。ArkTS 里这些不是可选项,全部强制开启:
noImplicitReturns:函数所有路径必须返回strictFunctionTypes:函数类型严格检查strictNullChecks:null 和 undefined 严格区分strictPropertyInitialization:类属性必须初始化
为什么要做这些取舍
看完限制清单,可能会觉得 ArkTS 把 TS 削得挺厉害。但理解了设计目标,这些取舍就说得通了。
ArkTS 的目标不是"更好的 TS",而是"HarmonyOS 应用开发的语言"。HarmonyOS 要跑在手机、平板、穿戴、车机、智慧屏各种设备上,有些设备资源有限。语言层面把性能和稳定性做扎实,比保留灵活性更重要。
每个限制都能对应到一个收益:
- 禁
any→ 编译器能在编译期确定类型,不用运行时检查 - 禁对象布局变更 → 属性访问可以按固定偏移量,不用查表
- 禁 structural typing → 不用运行时比较类型结构
- 强制严格检查 → 把 null 相关的错误在编译期就拦住
这些收益在单次操作上可能看不出差别,但应用跑起来,每帧 16ms 的预算里,省下来的都是真金白银。
从 TS 迁移到 ArkTS 的注意事项
把已有的 TS 代码迁到 ArkTS,按这个顺序处理会比较顺:
第一步,过类型检查。 把 TS 项目的 strict 打开,noImplicitAny 打开,先让 TS 严格模式能过。这一步能解决大部分问题——把 any 换成具体类型,把没标的返回类型补上,把 null 处理补全。
第二步,清理动态特性。 检查代码里有没有这些:
as any类型断言@ts-ignore注释- 运行时加属性
obj.newProp = value - 解构赋值(这个 TS 里太常见,要重点查)
- 函数表达式
function() {} Function.bind/apply/call
第三步,处理类型系统差异。 如果代码里用了这些 TS 高级类型特性,要重构:
- intersection type
A & B→ 改成接口继承 - conditional type
T extends U ? X : Y→ 改成显式泛型 - mapped type → 显式定义
- structural typing 依赖 → 加显式接口
第四步,UI 代码重写。 如果是 React/Vue 的 UI 代码,不能直接迁,要按 ArkUI 的声明式语法重写。这块没有自动化工具,得手动来。
来看一个迁移实例。TS 代码:
// 原始 TS 代码
interface Id { id: number; }
interface Name { name: string; }
type User = Id & Name; // intersection type
function getUser(): User {
return { id: 1, name: '张三' }; // 对象字面量没标类型
}
let { id, name } = getUser(); // 解构赋值
console.info(`${id}: ${name}`);
let users: any[] = [getUser()]; // any 类型
迁到 ArkTS:
// ArkTS 代码
interface Id {
id: number;
}
interface Name {
name: string;
}
// intersection type 改成接口继承
interface User extends Id, Name {}
class UserImpl implements User {
public id: number = 0;
public name: string = '';
}
function getUser(): User {
let u: UserImpl = { id: 1, name: '张三' }; // 标注类型
return u;
}
// 解构赋值改成逐个取
let user = getUser();
let id = user.id;
let name = user.name;
console.info(`${id}: ${name}`);
let users: User[] = [getUser()]; // any 改成具体类型
改动不大,但每个点都对应一个 ArkTS 限制。批量迁代码时,按这个套路处理能少走弯路。
总结一下下
先开 TS 严格模式再迁。 在 TS 项目里把 strict: true 配上,能过的代码迁到 ArkTS 就轻松大半。过不了的部分就是真正要改的。别直接拿松散的 TS 代码往 ArkTS 塞,报错会多到劝退。
解构赋值是最容易踩的坑。 TS 里 const { a, b } = obj 太常见了,迁到 ArkTS 全得改。批量替换可以用正则,但复杂解构(嵌套、默认值、rest)得手动改。
别指望 structural typing 帮你省代码。 TS 里两个类结构一样就能互相赋值,写起来省事。ArkTS 不行,得显式建接口。迁代码时如果依赖了这个特性,要补接口定义。
装饰器不是 TS 那种装饰器。 ArkTS 的 @Component、@State 这些是语言层面的,有特定语义,不是 TS 实验性装饰器那种通用机制。别用 TS 装饰器的思路去理解。
官方有适配规则文档。 遇到不确定的限制,查 从 TypeScript 到 ArkTS 的适配规则。文档里每个限制都有规则名(如 arkts-no-any-unknown)、错误码、TS 写法和 ArkTS 写法的对照,比凭感觉试错快得多。
更多推荐



所有评论(0)