ArkTS 语言基础与 TypeScript 的差异

本文从 ArkTS 运行于 ArkVM 与严格模式的定位讲起,梳理基础类型、interface、class、enum、泛型、函数重载、可选链等语言要点,并逐条给出 ArkTS 相对 TypeScript 的限制清单与正反例代码,覆盖禁用 any、禁止结构化类型、禁止动态增删属性、不支持声明合并等关键约束,附 TS 特性支持对照表与常见编译坑清单。

用 TypeScript 写惯了的人,第一天写 ArkTS 最常见的体验是:编辑器满屏红波浪线,报的错还都很有道理。ArkTS 不是"鸿蒙版的 TypeScript",而是 TypeScript 的一个静态类型子集加约束——它砍掉了几乎所有依赖运行时动态特性的语法,换来的是更高的运行效率和更强的编译期保障。本文把这份"砍掉了什么"的清单讲清楚。


一、ArkTS 是什么

1.1 运行于 ArkVM

ArkTS 代码经过编译后生成字节码,由 ArkVM 执行。这个链路决定了它和 TypeScript 的根本差异:TypeScript 是"类型擦除后变成 JavaScript",类型只在编译期存在;ArkTS 的类型信息在运行时是真实存在且可用的,因此运行时可以做更激进的优化——比如基于确定的类型布局做属性内联、跳过动态查找。

代价就是灵活性。所有让类型在运行时"不确定"的写法,ArkTS 一律禁止。

1.2 严格模式

ArkTS 默认开启严格模式,同时叠加了一组比 tsconfig 里 strict: true 更严的规则。可以把它理解为一个"没有逃生舱"的 TS:没有 any、没有 unknown、没有 @ts-ignore;类型必须能在编译期完全确定;原型修改、属性增删、eval 这类动态语言特性一律不可用。

1.3 .ets 与 .ts 的分工

鸿蒙工程里两种后缀并存:

后缀用途能否写 UI
.etsArkTS 源文件,可含装饰器与声明式 UI可以,@Entry/@Component 必须在此
.ts纯 TypeScript 逻辑文件不可以,但受 ArkTS 约束校验

实践中的分工是:UI 与状态管理写 .ets,纯计算逻辑、数据结构、工具函数写 .ts,第三方库通常只提供 .ts 声明文件。需要注意的是,.ts 文件同样会经过 ArkTS 规则校验(在鸿蒙工程内),所以不能因为后缀是 .ts 就随意用 any。


二、基础类型与语言要点

2.1 基础类型

ArkTS 保留了 TS 的基础类型体系,但对象类型必须用 interface 或 class 显式声明,不能用匿名对象类型。

interface UserProfile {
  id: number;
  name: string;
  vip: boolean;
  tags: string[];
  joinedAt: Date;
  extra?: Record<string, string>;
}

let count: number = 0;
let list: number[] = [1, 2, 3];
let maybe: string | null = null;

union 类型、字面量类型、Record、元组都支持,但联合类型的分支收窄(narrowing)能力比 TS 弱一些,复杂场景建议直接用 class 层次结构代替。

2.2 interface 与 class

interface Serializable {
  serialize(): string;
}

// 显式 implements,且必须实现全部成员
class Device implements Serializable {
  readonly name: string;
  private sn: string;

  constructor(name: string, sn: string) {
    this.name = name;
    this.sn = sn;
  }

  serialize(): string {
    return JSON.stringify({ name: this.name, sn: this.sn });
  }
}

注意 implements 是强制且有效的:ArkTS 禁止结构化类型,所以即使一个类"长得像"某个接口,没有 implements 就不算实现了它。

2.3 enum

enum NetState {
  IDLE = 0,
  CONNECTED = 2,
  FAILED = 3
}

// 字符串枚举同样支持,可读性更好
enum LogLevel {
  DEBUG = 'DEBUG',
  ERROR = 'ERROR'
}

ArkTS 不支持 const enum 的跨文件内联用法,建议统一用普通 enum。

2.4 泛型

泛型是 ArkTS 的强项,约束(extends)与默认类型参数都支持:

interface Repository<T extends object> {
  getById(id: string): Promise<T | null>;
  save(item: T): Promise<void>;
}

class MemoryRepo<T extends object> implements Repository<T> {
  private store: Map<string, T> = new Map();

  async getById(id: string): Promise<T | null> {
    return this.store.get(id) ?? null;
  }

  async save(item: T): Promise<void> {
    this.store.set(JSON.stringify(item), item);
  }
}

2.5 函数重载

ArkTS 支持函数重载声明,但限制较多:重载签名必须连续书写,实现签名只能有一个,且实现签名必须兼容所有重载签名。

// 重载签名在前,实现签名在最后
function format(value: number): string;
function format(value: string): string;
function format(value: number | string): string {
  if (typeof value === 'number') {
    return value.toFixed(2);
  }
  return value.trim();
}

实践中更推荐用可选参数或联合类型替代重载,减少编译器的解析负担。

2.6 可选链与空值合并

interface Config {
  network?: {
    timeout?: number;
  };
}

const cfg: Config = {};
// 可选链 + 空值合并,是 ArkTS 里处理可空值的首选写法
const timeout: number = cfg.network?.timeout ?? 5000;

// 非空断言 ! 允许使用,但要谨慎
const arr: string[] | null = null;
const len: number = arr?.length ?? 0;

三、ArkTS 相对 TypeScript 的限制清单

这一节是全文重点。每条都给出反例(TS 能过、ArkTS 报错)与正例。

3.1 禁用 any 与 unknown

// 反例:报错 Use explicit types instead of "any"
// function parse(input: any): any { return JSON.parse(input); }

// 正例:用明确的接口
interface ApiResp {
  code: number;
  msg: string;
}

function parseResp(raw: string): ApiResp {
  return JSON.parse(raw) as ApiResp;
}

JSON.parse 的返回值在 ArkTS 中是 Object,必须 as 断言到明确类型后才能取字段,这是最高频的编译错误来源之一。

3.2 禁止结构化类型

TypeScript 的"鸭子类型"在 ArkTS 中不成立:字段相同但没有继承关系的两个类型不能互相赋值。

interface Point {
  x: number;
  y: number;
}

// 反例:Point2 字段与 Point 一致,但没有 implements 关系,以下两行编译失败
// class Point2 { x: number = 0; y: number = 0; }
// const p: Point = new Point2();

// 正例:显式 implements
class PointImpl implements Point {
  x: number = 0;
  y: number = 0;
}
const q: Point = new PointImpl();    // 通过

3.3 禁止运行时动态增删对象属性

interface Person {
  name: string;
  age?: number;      // 需要扩展的字段必须提前声明
}

const p: Person = { name: 'Tom' };
p.age = 18;            // 合法
// p['weight'] = 60;   // 编译报错:属性不存在
// delete p.name;      // 编译报错:不支持 delete 对象属性

如果确实需要动态键值,用 Map<string, T> 或 Record<string, T>,不要用对象。

3.4 对象字面量必须有明确类型

// 反例:报错 Object literal must correspond to some explicitly declared class or interface
// const obj = { name: 'a', value: 1 };

interface Item {
  name: string;
  value: number;
}

// 正例一:显式标注类型
const item: Item = { name: 'a', value: 1 };

// 正例二:作为实参时由形参提供上下文类型
function send(it: Item): void { console.info(it.name); }
send({ name: 'b', value: 2 });

3.5 不支持声明合并

// 反例:ArkTS 不支持同名 interface 合并,报重复标识符
// interface Box { a: number; }
// interface Box { b: number; }

// 正例:一次写全,或用 extends 继承
interface BoxBase {
  a: number;
}
interface Box extends BoxBase {
  b: number;
}

同理,namespace 与 interface 的合并、函数与命名空间的合并都不可用。

3.6 限制 as 类型断言

// 反例:TS 允许的双重断言绕过检查,ArkTS 禁止
// const n = ('abc' as unknown) as number;

// 正例:断言目标必须是相关类型,或用类型守卫
function isApiResp(v: Object): v is ApiResp {
  return typeof (v as ApiResp).code === 'number';
}
const raw: Object = JSON.parse('{"code":0,"msg":"ok"}');
// 类型守卫收窄后可安全赋值
if (isApiResp(raw)) { const r: ApiResp = raw; }

3.7 禁止 Symbol 与原型操作

// 反例:以下写法全部禁止
// const s = Symbol('k');
// Object.setPrototypeOf(obj, proto);
// obj.__proto__ = proto;
// SomeClass.prototype.newMethod = () => {};

// 正例:用类继承或组合表达扩展
class Base {
  hello(): string { return 'base'; }
}
class Derived extends Base {
  hello(): string { return 'derived'; }
}

3.8 类字段必须初始化

// 反例:报错 Property 'count' has no initializer
// class Bad { count: number; }

// 正例:声明时给初值,或在构造函数中赋值
class Counter {
  count: number = 0;
  name?: string;          // 可选属性可以不初始化
  private step: number;

  constructor(step: number) {
    this.step = step;     // 构造函数内赋值也算初始化
  }
}

3.9 限制 Function 类型与 call/apply/bind

// 反例:ArkTS 不支持 Function 类型
// let fn: Function = () => {};

// 正例:用明确的函数签名类型
type Handler = (code: number, msg: string) => void;

const onDone: Handler = (code, msg) => { console.info(`${code}: ${msg}`); };
onDone(0, 'ok');      // 直接调用,避免 apply 的动态参数数组

bind 在部分场景可用,但推荐的替代方案是箭头函数捕获 this。

3.10 索引签名与装饰器元数据反射

// 反例:ArkTS 不支持任意索引签名
// interface Dict { [key: string]: number; }

// 正例:用 Record 或 Map
type Dict = Record<string, number>;
const d: Dict = { a: 1, b: 2 };
const m: Map<string, number> = new Map();
m.set('a', 1);

此外,TS 通过 reflect-metadata 实现的装饰器运行时反射在 ArkTS 中不可用。ArkTS 的装饰器是编译期处理的,能力由 ArkUI 框架提供,不能自定义一个靠反射读元数据的装饰器框架。


四、TS 特性与 ArkTS 支持情况对照表

TS 特性ArkTS 支持情况替代方案
any / unknown不支持显式 interface、泛型、联合类型
结构化类型(鸭子类型)不支持显式 implements / extends
动态增删对象属性不支持可选属性、Map、Record
匿名对象字面量推断不支持显式类型标注或 class 构造
声明合并不支持一次写全或 extends 继承
双重 as 断言不支持类型守卫函数 v is T
Symbol不支持字符串常量、enum
原型操作与 __proto__不支持类继承、组合
未初始化类字段不支持声明时初始化或构造函数赋值
Function 类型不支持明确的函数签名 type
call / apply部分限制直接调用、展开运算符、箭头函数
任意索引签名不支持Record<K, V>、Map<K, V>
装饰器元数据反射不支持使用 ArkUI 内置装饰器
namespace不支持ES Module(import / export)
泛型与泛型约束完全支持—
enum(数字与字符串)完全支持—
可选链 ?. 与空值合并 ??完全支持—
类型守卫 v is T完全支持—
函数重载受限支持联合类型参数、可选参数
readonly完全支持—

五、ArkTS 装饰器概览

ArkTS 装饰器是语言层与 ArkUI 框架层的结合点。这里只做引入,具体用法会在状态管理与 UI 章节展开。

5.1 状态管理装饰器

装饰器作用对象语义
@State组件内部变量组件私有的响应式状态
@Prop子组件变量父传子的单向同步
@Link子组件变量父子双向同步
@Provide / @Consume跨层级祖先提供、后代消费
@Observed / @ObjectLink类与引用观察类实例的深层属性变化
@Watch状态变量状态变化时的回调

5.2 结构类装饰器

装饰器作用
@Entry标记页面入口,一个页面文件只能有一个
@Component标记自定义组件,必须实现 build()
@Builder轻量 UI 复用函数
@Styles / @Extend样式复用
@CustomDialog自定义弹窗组件
// 一个最小但完整的 ArkTS 组件,注意 struct 关键字
@Component
struct Counter {
  @State count: number = 0;

  build() {
    Column({ space: 12 }) {
      Text(`count = ${this.count}`)
        .fontSize(20)
      Button('加一')
        .onClick(() => { this.count += 1; })
    }
    .padding(16)
  }
}

注意 @Component 修饰的是 struct 而不是 class,这是 ArkTS 独有的语法。这部分内容会在 ArkUI 声明式 UI 与状态管理 中详细展开。


六、常见坑清单

  1. 习惯性写 any 导致编译失败:最常见的报错是 Use explicit types instead of "any"。不要试图用 // @ts-ignore 绕过,ArkTS 不支持这个注释指令,正确做法是定义接口或使用泛型。
  2. 对象字面量推断失败:const x = { a: 1 } 在 ArkTS 中直接报错。要么标注类型,要么把字面量作为函数实参传入(此时由形参类型提供上下文类型)。
  3. 跨文件类型未导出:.ets 与 .ts 之间的类型引用必须显式 export,且引用路径区分大小写。遗漏导出时报错信息会指向使用处,容易误判。
  4. JSON.parse 返回值处理:返回值类型是 Object,直接取字段会编译失败。必须先 as 断言到接口,且要自行校验字段是否存在——断言不会做运行时检查。
  5. 结构化类型不兼容:两个字段完全一致的 interface 之间也不能互相赋值,必须用 extends 建立继承关系,或在设计之初就共用同一个接口。
  6. Map 与普通对象混用:想用 obj[key] 动态取值时会被拦下,改 Map 后又忘了 Map 的响应式支持有限(@State 对 Map 的变化检测需要配合特定版本 API),建议状态用 class + @Observed。
  7. 重载签名顺序错误:实现签名写在重载签名之前会直接报错,且报错信息不直观,检查时优先看顺序。另外,写惯了 React/Vue 的人容易把组件写成 class,ArkTS 组件必须是 struct,且不能有构造函数。

如果你后续要处理并发任务,会发现 ArkTS 的严格类型约束在 TaskPool 与 Worker 的序列化场景下会带来额外要求——跨线程传递的对象必须可序列化,且不能含函数成员,详见 HarmonyOS 并发编程与 TaskPool 。另外,从 Flutter 架构与设计模式 迁移过来的团队,需要特别注意 ArkTS 没有 Dart 那样的 dynamic 逃生通道,架构分层必须提前设计好数据模型。


小结

ArkTS 的核心心智是"用灵活性换确定性":它砍掉 any、结构化类型、动态属性、原型操作这些让类型在运行时不确定的写法,换来的是 ArkVM 可以做更激进的优化,以及编译期能捕获更多错误。实践中的三条准则是:所有数据结构先定义 interface 或 class,所有外部输入(网络、文件、JSON.parse)先断言再校验,所有跨文件类型显式导出。做到这三点,绝大多数 ArkTS 编译错误都会在写代码时自然规避。装饰器与 UI 语法是 ArkTS 的另一半,属于框架层而非语言层,需要结合 ArkUI 的组件与状态管理一起理解。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「鸿蒙开发」更多文章

  1. 鸿蒙 ohpm 包管理与 Hypium 测试框架
  2. ArkUI 动画体系与手势交互
  3. 鸿蒙应用安全:权限模型与 HUKS 密钥管理