用 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 |
|---|---|---|
.ets | ArkTS 源文件,可含装饰器与声明式 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 与状态管理
中详细展开。
六、常见坑清单
- 习惯性写
any导致编译失败:最常见的报错是Use explicit types instead of "any"。不要试图用// @ts-ignore绕过,ArkTS 不支持这个注释指令,正确做法是定义接口或使用泛型。 - 对象字面量推断失败:
const x = { a: 1 }在 ArkTS 中直接报错。要么标注类型,要么把字面量作为函数实参传入(此时由形参类型提供上下文类型)。 - 跨文件类型未导出:
.ets与.ts之间的类型引用必须显式export,且引用路径区分大小写。遗漏导出时报错信息会指向使用处,容易误判。 JSON.parse返回值处理:返回值类型是Object,直接取字段会编译失败。必须先as断言到接口,且要自行校验字段是否存在——断言不会做运行时检查。- 结构化类型不兼容:两个字段完全一致的 interface 之间也不能互相赋值,必须用
extends建立继承关系,或在设计之初就共用同一个接口。 Map与普通对象混用:想用obj[key]动态取值时会被拦下,改Map后又忘了Map的响应式支持有限(@State对Map的变化检测需要配合特定版本 API),建议状态用class+@Observed。- 重载签名顺序错误:实现签名写在重载签名之前会直接报错,且报错信息不直观,检查时优先看顺序。另外,写惯了 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 的组件与状态管理一起理解。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。