写过 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

这是最直接的限制。anyunknown 在 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必须用 letconstlet
禁止函数表达式不能写 function() {}用箭头函数 () => {}
禁止函数内声明函数不能在函数里 function foo()用 lambda
禁止 Function.apply/call/bindthis 语义限制用 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 的严格模式是可配置的,strictNullChecksnoImplicitReturns 这些可以开关。ArkTS 里这些不是可选项,全部强制开启:

  • noImplicitReturns:函数所有路径必须返回
  • strictFunctionTypes:函数类型严格检查
  • strictNullChecks:null 和 undefined 严格区分
  • strictPropertyInitialization:类属性必须初始化

为什么要做这些取舍

看完限制清单,可能会觉得 ArkTS 把 TS 削得挺厉害。但理解了设计目标,这些取舍就说得通了。

ArkTS 的目标不是"更好的 TS",而是"HarmonyOS 应用开发的语言"。HarmonyOS 要跑在手机、平板、穿戴、车机、智慧屏各种设备上,有些设备资源有限。语言层面把性能和稳定性做扎实,比保留灵活性更重要。

ArkTS 设计目标

高性能

高稳定性

开发期错误检查

静态类型 → 减少运行时检查

固定对象布局 → 按偏移访问属性

AOT 编译 → 热点代码编译执行

禁止 any → 类型确定

禁止对象布局变更 → 行为可预测

Actor 并发 → 无锁竞争

强制严格检查 → 编译期暴露问题

禁止 @ts-ignore → 不能跳过检查

每个限制都能对应到一个收益:

  • 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 写法的对照,比凭感觉试错快得多。

Logo

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

更多推荐